Skip to content

Commit

Permalink
WIP: reoder-pages-in-viewer (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
ciur authored Sep 23, 2023
1 parent 4d00a05 commit 0e76b5f
Show file tree
Hide file tree
Showing 18 changed files with 562 additions and 46 deletions.
93 changes: 93 additions & 0 deletions papermerge/core/page_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import io
import os
from pathlib import Path
from typing import List

from pikepdf import Pdf

from papermerge.core.models import Page
from papermerge.core.schemas import DocumentVersion as PyDocVer
from papermerge.core.schemas.pages import PageAndRotOp
from papermerge.core.storage import abs_path, get_storage_instance


def apply_pages_op(items: List[PageAndRotOp]):
pages = Page.objects.filter(
pk__in=[item.page.id for item in items]
)
old_version = pages.first().document_version

doc = old_version.document
new_version = doc.version_bump(
page_count=len(items)
)

reorder_pdf_pages(
old_version=old_version,
new_version=new_version,
items=items
)


def reorder_pdf_pages(
old_version: PyDocVer,
new_version: PyDocVer,
items: List[PageAndRotOp]
):
src = Pdf.open(abs_path(old_version.document_path.url))

dst = Pdf.new()

for item in items:
page = src.pages.p(item.page.number)
dst.pages.append(page)

dirname = os.path.dirname(
abs_path(new_version.document_path.url)
)
os.makedirs(dirname, exist_ok=True)
dst.save(abs_path(new_version.document_path.url))


def reuse_ocr_data(uuid_map) -> None:
storage_instance = get_storage_instance()

for src_uuid, dst_uuid in uuid_map.items():
storage_instance.copy_page(
src=Path("pages", src_uuid),
dst=Path("pages", dst_uuid)
)


def reuse_text_field(
old_version: PyDocVer,
new_version: PyDocVer,
page_map: list
) -> None:
streams = collect_text_streams(
version=old_version,
# list of old_version page numbers
page_numbers=[item[1] for item in page_map]
)

# updates page.text fields and document_version.text field
new_version.update_text_field(streams)


def collect_text_streams(
version: PyDocVer,
page_numbers: list[int]
) -> list[io.StringIO]:
"""
Returns list of texts of given page numbers from specified document version
Each page's text is wrapped as io.StringIO instance.
"""
pages_map = {page.number: page for page in version.pages.all()}

result = [
io.StringIO(pages_map[number].text)
for number in page_numbers
]

return result
21 changes: 21 additions & 0 deletions papermerge/core/routers/pages.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging
import os
import uuid
from typing import List

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse

from papermerge.core.constants import DEFAULT_THUMBNAIL_SIZE
from papermerge.core.models import Page, User
from papermerge.core.page_operations import apply_pages_op
from papermerge.core.pathlib import rel2abs, thumbnail_path
from papermerge.core.schemas.documents import DocumentVersion as PyDocVer
from papermerge.core.schemas.pages import PageAndRotOp
from papermerge.core.storage import abs_path

from .auth import get_current_user as current_user
Expand Down Expand Up @@ -94,3 +98,20 @@ def get_page_jpg_url(
logger.debug(f"jpeg_abs_path={jpeg_abs_path}")

return JPEGFileResponse(jpeg_abs_path)


@router.post("/")
def apply_page_operations(
items: List[PageAndRotOp],
) -> List[PyDocVer]:
"""Applies reorder, delete and/or rotate operation(s) on a set of pages.
Creates a new document version which will contain
only the pages provided as input in given order and with
applied rotation. The deletion operation is implicit:
pages not included in input won't be added to the new document version
which from user perspective means that pages were deleted.
Order in which input pages are provided is very important because
new document version will add pages in exact same order.
"""
apply_pages_op(items)
13 changes: 13 additions & 0 deletions papermerge/core/schemas/pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from uuid import UUID

from pydantic import BaseModel


class Page(BaseModel):
id: UUID
number: int


class PageAndRotOp(BaseModel):
page: Page
ccw: int = 0
1 change: 0 additions & 1 deletion ui/src/components/commander/commander.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Menu from './menu';
import { DraggingIcon } from 'components/dragging_icon';

import { is_empty } from 'utils/misc';
import { get_node_under_cursor } from 'utils/misc';
import { fetcher } from 'utils/fetcher';

import DeleteNodesModal from 'components/modals/delete_nodes';
Expand Down
27 changes: 27 additions & 0 deletions ui/src/components/viewer/action_panel/action_panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Button } from "react-bootstrap"
import UnappliedPageOpChanges from "./unapplied_page_op_changes"

type Args = {
unapplied_page_op_changes: boolean;
onApplyPageOpChanges: () => void;
}

export default function ActionPanel({
unapplied_page_op_changes,
onApplyPageOpChanges
}: Args) {
return (
<div className="action-panel">
<Button className="rounded-0 m-1" variant="success">
<i className="bi bi-pencil-square me-1"></i>Rename
</Button>
<Button className="rounded-0 m-1" variant="success">
<i className="bi bi-cloud-download me-1"></i>Download
</Button>
{
unapplied_page_op_changes &&
<UnappliedPageOpChanges
onClick={onApplyPageOpChanges}/>}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState } from "react";
import { Button, Spinner } from "react-bootstrap"


type Args = {
onClick: () => void;
}

export default function UnappliedPageOpChanges({onClick}: Args) {
const [inProgress, setInProgress] = useState(false);

const onLocalClick = () => {
setInProgress(true);
onClick();
}

return (
<span className="unapplied-page-op-changes">
Unapplied pages operations detected
<Button disabled={inProgress}
className="rounded-0 m-1" variant="success"
onClick={onLocalClick}>
{inProgress && <Spinner />} Apply
</Button>
</span>
);
}
12 changes: 6 additions & 6 deletions ui/src/components/viewer/pages_panel/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useRef } from 'react';
import PagePlaceholder from './page_placeholder';

import type { PageType } from "types"
import type { PageAndRotOp } from "types"
import { useProtectedSVG } from "hooks/protected_image"

type Args = {
page: PageType;
item: PageAndRotOp;
/*
if `scroll_into_view`=True -> page should be scrolled into the view.
Expand Down Expand Up @@ -33,14 +33,14 @@ function get_page_panel_width(): number {
return ret;
}

export function Page({page, scroll_into_view}: Args) {
export function Page({item, scroll_into_view}: Args) {

//const base64_jpg = useProtectedJpg(page.jpg_url);
const pageRef = useRef<HTMLDivElement | null>(null);

let {data, is_loading, error} = useProtectedSVG(
page.svg_url,
`${page.jpg_url}?size=${get_page_panel_width()}`
item.page.svg_url,
`${item.page.jpg_url}?size=${get_page_panel_width()}`
);
let page_component: JSX.Element | null

Expand All @@ -52,7 +52,7 @@ export function Page({page, scroll_into_view}: Args) {
page_component = <div>
{data}
<div className='p-2 mb-3 page-number text-center'>
{page.number}
{item.page.number}
</div>
</div>
}
Expand Down
14 changes: 7 additions & 7 deletions ui/src/components/viewer/pages_panel/pages_panel.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Page } from "./page";
import type { PageType } from "types"
import type { PageAndRotOp } from "types"

type Args = {
pages: Array<PageType>;
items: Array<PageAndRotOp>;
current_page_number: number;
}

export function PagesPanel({pages, current_page_number}: Args) {
export function PagesPanel({items, current_page_number}: Args) {
return (
<div className='pages-panel flex-grow-1'>
{pages.map(page => <Page
key={page.id}
page={page}
scroll_into_view={page.number == current_page_number}/>)}
{items.map(item => <Page
key={item.page.id}
item={item}
scroll_into_view={item.page.number == current_page_number}/>)}
</div>
);
}
1 change: 1 addition & 0 deletions ui/src/components/viewer/thumbnails_panel/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PAGE_ID = "page_id";
13 changes: 13 additions & 0 deletions ui/src/components/viewer/thumbnails_panel/page_thumbnail.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

.page-thumbnail {
border-top: 3px solid #666;
border-bottom: 3px solid #666;
}

.borderline-top {
border-top-color: blue;
}

.borderline-bottom {
border-bottom-color: blue;
}
Loading

0 comments on commit 0e76b5f

Please sign in to comment.