PageControls

This commit is contained in:
Johannes Zillmann 2021-03-09 08:17:50 +01:00
parent 8b7ae34bbc
commit 45355a9315
14 changed files with 164 additions and 128 deletions

View File

@ -12,14 +12,22 @@ export default class Debugger {
private context: TransformContext;
private transformers: ItemTransformer[];
private stageResultCache: StageResult[];
pageCount: number;
fontMap: Map<string, object>;
stageNames: string[];
stageDescriptions: string[];
constructor(inputSchema: string[], inputItems: Item[], context: TransformContext, transformers: ItemTransformer[]) {
constructor(
pageCount: number,
inputSchema: string[],
inputItems: Item[],
context: TransformContext,
transformers: ItemTransformer[],
) {
this.transformers = transformers;
this.context = context;
this.fontMap = context.fontMap;
this.pageCount = pageCount;
this.stageNames = ['Parse Result', ...transformers.map((t) => t.name)];
this.stageDescriptions = ['Initial items as parsed by PDFjs', ...transformers.map((t) => t.description)];
this.stageResultCache = [initialStage(inputSchema, inputItems)];

View File

@ -3,30 +3,13 @@ import type Metadata from './Metadata';
import type PageViewport from './parse/PageViewport';
export default class ParseResult {
fontMap: Map<string, object>;
pdfjsPages: any[];
pageViewports: PageViewport[];
metadata: Metadata;
schema: string[];
items: Item[];
constructor(
fontMap: Map<string, object>,
pdfjsPages: any[],
pageViewports: PageViewport[],
metadata: Metadata,
schema: string[],
items: Item[],
) {
this.fontMap = fontMap;
this.pdfjsPages = pdfjsPages;
this.pageViewports = pageViewports;
this.metadata = metadata;
this.schema = schema;
this.items = items;
}
pageCount(): number {
return this.pdfjsPages.length;
}
public fontMap: Map<string, object>,
public pageCount: number,
public pdfjsPages: any[],
public pageViewports: PageViewport[],
public metadata: Metadata,
public schema: string[],
public items: Item[],
) {}
}

View File

@ -23,6 +23,7 @@ export default class PdfParser {
.promise.then((pdfjsDocument: any) => {
reporter.parsedDocumentHeader(pdfjsDocument.numPages);
return Promise.all([
pdfjsDocument,
pdfjsDocument.getMetadata().then((pdfjsMetadata: any) => {
reporter.parsedMetadata();
return new Metadata(pdfjsMetadata);
@ -30,10 +31,15 @@ export default class PdfParser {
this.extractPagesSequentially(pdfjsDocument, reporter),
]);
})
.then(([metadata, pages]: [Metadata, ParsedPage[]]) => {
return Promise.all([metadata, pages, this.gatherFontObjects(pages).finally(() => reporter.parsedFonts())]);
.then(([pdfjsDocument, metadata, pages]: [any, Metadata, ParsedPage[]]) => {
return Promise.all([
pdfjsDocument,
metadata,
pages,
this.gatherFontObjects(pages).finally(() => reporter.parsedFonts()),
]);
})
.then(([metadata, pages, fontMap]: [Metadata, ParsedPage[], Map<string, object>]) => {
.then(([pdfjsDocument, metadata, pages, fontMap]: [any, Metadata, ParsedPage[], Map<string, object>]) => {
const pdfjsPages = pages.map((page: any) => page.pdfjsPage);
const items = pages.reduce((allItems: any[], page: any) => allItems.concat(page.items), []);
const pageViewports = pdfjsPages.map((page: any) => {
@ -43,7 +49,15 @@ export default class PdfParser {
this.pdfjs.Util.transform(viewPort.transform, itemTransform),
};
});
return new ParseResult(fontMap, pdfjsPages, pageViewports, metadata, this.schema, items);
return new ParseResult(
fontMap,
pdfjsDocument.numPages,
pdfjsPages,
pageViewports,
metadata,
this.schema,
items,
);
});
}

View File

@ -39,7 +39,7 @@ export default class PdfPipeline {
async debug(src: string | Uint8Array | object, progressListener: ProgressListenFunction): Promise<Debugger> {
const parseResult = await this.parse(src, progressListener);
const context = { fontMap: parseResult.fontMap, pageViewports: parseResult.pageViewports };
return new Debugger(parseResult.schema, parseResult.items, context, this.transformers);
return new Debugger(parseResult.pageCount, parseResult.schema, parseResult.items, context, this.transformers);
}
/**

View File

@ -22,7 +22,7 @@ export default class StageResult {
}, []);
}
selectPages(relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number): Page[] {
selectPages(relevantChangesOnly: boolean, groupItems: boolean): Page[] {
let result: Page[];
// Ungroup pages
@ -32,11 +32,6 @@ export default class StageResult {
result = this.pages;
}
// Filter to pinned page
if (Number.isInteger(pinnedPage)) {
result = result.filter((page) => page.index === pinnedPage);
}
// Filter out item (groups) with no changes
if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) {
result = result.map(

View File

@ -36,7 +36,7 @@ describe('Transform Items', () => {
const trans1Items = parsedItems.map((item) => item.withData({ C: `c=${item.value('A')}+${item.value('B')}` }));
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual(parsedSchema.map((column) => ({ name: column })));
@ -62,7 +62,7 @@ describe('Transform Items', () => {
const trans1Items = parsedItems.map((item) => item.withData({ line: item.data['y'] }));
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'y' }]);
@ -100,7 +100,7 @@ test('Change inside of Line', async () => {
const trans1Items = swapElements([...parsedItems], 0, 1);
const transformers = [new TestTransformer('Trans1', trans1Desc, trans1Schema, trans1Items)];
const debug = new Debugger(parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
const debug = new Debugger(1, parsedSchema, parsedItems, { fontMap: new Map(), pageViewports: [] }, transformers);
expect(debug.stageNames).toEqual(['Parse Result', 'Trans1']);
expect(debug.stageResults(0).schema).toEqual([{ name: 'id' }, { name: 'line' }]);
@ -134,7 +134,7 @@ describe('build schemas', () => {
function calculateSchema(inputSchema: string[], outputSchema: string[]): AnnotatedColumn[] {
const transformers = [new TestTransformer('Trans1', {}, outputSchema, items)];
const debug = new Debugger(inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers);
const debug = new Debugger(1, inputSchema, items, { fontMap: new Map(), pageViewports: [] }, transformers);
return debug.stageResults(1).schema;
}

View File

@ -21,7 +21,7 @@ test('basic example PDF parse', async () => {
const expectedPages = 7;
expect(result.metadata.title()).toEqual('ExamplePdf');
expect(result.metadata.author()).toEqual('Johannes Zillmann');
expect(result.pageCount()).toBe(expectedPages);
expect(result.pageCount).toBe(expectedPages);
result.pdfjsPages.forEach((pdfPage, i) => {
expect(pdfPage._pageIndex).toBe(i);
});

View File

@ -5,7 +5,6 @@ import AnnotatedColumn from 'src/debug/AnnotatedColumn';
import Page, { asPages } from 'src/debug/Page';
import { items } from '../testItems';
import LineItemMerger from 'src/debug/LineItemMerger';
import ItemGroup from 'src/debug/ItemGroup';
test('itemsUnpacked', async () => {
const tracker = new ChangeTracker();
@ -77,22 +76,6 @@ describe('select pages', () => {
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]);
const allGroupedWithPin = result.selectPages(false, true, 0);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[0, 1], [2]]);
const relevantGroupedWithPin = result.selectPages(true, true, 0);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[0, 1]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 0);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([]);
const allUnpackedWithPin = result.selectPages(false, false, 0);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[0], [1], [2]]);
});
test('Changes on element level', async () => {
@ -141,22 +124,6 @@ describe('select pages', () => {
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]);
const allGroupedWithPin = result.selectPages(false, true, 2);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[4, 5], [6]]);
const relevantGroupedWithPin = result.selectPages(true, true, 2);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[4, 5]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 2);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([[5]]);
const allUnpackedWithPin = result.selectPages(false, false, 2);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[4], [5], [6]]);
});
test('showAll - grouped', async () => {

View File

@ -8,8 +8,8 @@
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import { debugStage } from '../config';
import type StageResult from '@core/debug/StageResult';
import PageControl from './PageControl';
import Popup from '../components/Popup.svelte';
import PageSelectionPopup from './PageSelectionPopup.svelte';
import Checkbox from '../components/Checkbox.svelte';
@ -18,26 +18,24 @@
export let stageNames: string[];
export let stageDescriptions: string[];
export let pageControl: PageControl;
export let fontMap: Map<string, object>;
export let stageResult: StageResult;
export let supportsGrouping: boolean;
export let supportsRelevanceFiltering: boolean;
export let pinnedPage: number;
export let groupingEnabled = true;
export let onlyRelevantItems = true;
let { pinnedPageIndex, pagePinned } = pageControl;
$: canNext = $debugStage + 1 < stageNames.length;
$: canPrev = $debugStage > 0;
$: pagesNumbers = new Set(stageResult.pages.map((page) => page.index));
$: maxPage = Math.max(...pagesNumbers);
$: pageIsPinned = !isNaN(pinnedPage);
</script>
<div class="sticky top-0 pt-2 pb-1 z-20 bg-gray-50">
<div class="flex items-center space-x-2">
{#if pageIsPinned}
<span on:click={() => (pinnedPage = undefined)} transition:slideH={{ duration: 180, easing: linear }}>
{#if $pagePinned}
<span on:click={() => pageControl.unpinPage()} transition:slideH={{ duration: 180, easing: linear }}>
<Icon class="text-xs hover:text-select hover:opacity-25 cursor-pointer opacity-75" icon={pin} />
</span>
{/if}
@ -47,12 +45,7 @@
<BookOpen size="1x" class="hover:text-select cursor-pointer {opened && 'text-select'}" />
</span>
<span slot="content">
<PageSelectionPopup
{pagesNumbers}
{maxPage}
{pinnedPage}
on:pinPage={(e) => (pinnedPage = e.detail)}
on:unpinPage={() => (pinnedPage = undefined)} />
<PageSelectionPopup {pageControl} />
</span>
</Popup>
</span>
@ -80,10 +73,10 @@
<div>|</div>
<div>Transformation:</div>
<span on:click={() => canPrev && debugStage.update((cur) => cur - 1)}>
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
<ArrowLeft size="1x" class={canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span on:click={() => canNext && debugStage.update((cur) => cur + 1)}>
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
<ArrowRight size="1x" class={canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span>
<Popup>

View File

@ -7,19 +7,20 @@
import { debugStage } from '../config';
import ControlBar from './ControlBar.svelte';
import ItemTable from './ItemTable.svelte';
import PageControl from './PageControl';
export let debug: Debugger;
const pageControl = new PageControl(debug.pageCount);
const stageNames = debug.stageNames;
let pinnedPage: number;
const { pinnedPageIndex } = pageControl;
let groupingEnabled = true;
let onlyRelevantItems = true;
$: stageResult = debug.stageResults($debugStage);
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
$: pageIsPinned = !isNaN(pinnedPage);
$: visiblePages = stageResult.selectPages(onlyRelevantItems, groupingEnabled, pinnedPage);
$: visiblePages = pageControl.selectPages(stageResult, onlyRelevantItems, groupingEnabled, $pinnedPageIndex);
</script>
<div class="mx-4">
@ -31,13 +32,12 @@
<ControlBar
{stageNames}
stageDescriptions={debug.stageDescriptions}
{pageControl}
fontMap={debug.fontMap}
{stageResult}
{supportsGrouping}
{supportsRelevanceFiltering}
bind:groupingEnabled
bind:onlyRelevantItems
bind:pinnedPage />
bind:onlyRelevantItems />
<!-- Stage Messages -->
<ul class="messages list-disc list-inside mb-2 p-2 bg-blue-50 rounded shadow text-sm">
@ -48,7 +48,7 @@
<!-- Items -->
{#if visiblePages.find((page) => page.itemGroups.length > 0)}
<ItemTable schema={stageResult.schema} pages={visiblePages} {pageIsPinned} changes={stageResult.changes} />
<ItemTable schema={stageResult.schema} pages={visiblePages} {pageControl} changes={stageResult.changes} />
{:else}
<!-- No items visible -->
<div class="flex space-x-1 items-center justify-center text-xl">

View File

@ -1,5 +1,7 @@
<script>
import { scale, fade } from 'svelte/transition';
import { fade } from 'svelte/transition';
import { ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import type ItemGroup from '@core/debug/ItemGroup';
import type ChangeIndex from '@core/debug/ChangeIndex';
@ -7,15 +9,16 @@
import ChangeSymbol from './ChangeSymbol.svelte';
import { formatValue } from './formatValues';
import PageControl from './PageControl';
export let pageControl: PageControl;
export let pageIdx: number;
export let itemIdx: number;
export let schema: AnnotatedColumn[];
export let itemGroup: ItemGroup;
export let changes: ChangeIndex;
export let maxPage: number;
export let pageIsPinned: boolean;
let { pagePinned, canPrev, canNext } = pageControl;
let expandedItemGroup: { pageIndex: number; itemIndex: number };
$: expanded =
@ -36,7 +39,17 @@
<!-- Page number in first page item row -->
{#if itemIdx === 0}
<td id="page" class="page bg-gray-50 align-top">
<div>Page {pageIdx} {pageIsPinned ? '' : ' / ' + maxPage}</div>
<div>Page {pageIdx + 1} {$pagePinned ? '' : ' / ' + pageControl.totalPages}</div>
{#if $pagePinned}
<div class="absolute flex ml-1 space-x-2">
<span on:click={() => pageControl.prev()}>
<ArrowLeft size="1x" class={$canPrev ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
<span on:click={() => pageControl.next()}>
<ArrowRight size="1x" class={$canNext ? 'hover:text-select cursor-pointer' : 'opacity-25'} />
</span>
</div>
{/if}
</td>
{:else}
<td id="page" />

View File

@ -7,21 +7,22 @@
import ColumnAnnotation from '../../../core/src/debug/ColumnAnnotation';
import inView from '../actions/inView';
import PageControl from './PageControl';
import ItemRow from './ItemRow.svelte';
export let schema: AnnotatedColumn[];
export let pages: Page[];
export let pageIsPinned: boolean;
export let pageControl: PageControl;
export let changes: ChangeIndex;
let { pagePinned } = pageControl;
let maxPage = pages[pages.length - 1].index;
let maxItemsToRenderInOneLoad = 200;
let renderedMaxPage = 0;
let expandedItemGroup: { pageIndex: number; itemIndex: number };
let renderedPages: Page[];
$: {
if (pageIsPinned) {
if ($pagePinned) {
renderedPages = pages;
renderedMaxPage = 0;
} else {
@ -44,13 +45,6 @@
}
// console.log(`Render pages 0 to ${renderedMaxPage} with ${itemCount} items`);
}
const isExpanded = (pageIndex: number, itemIndex: number) => {
return expandedItemGroup?.pageIndex === pageIndex && expandedItemGroup?.itemIndex === itemIndex;
};
const toggleRow = (pageIndex: number, itemIndex: number) => {
expandedItemGroup = isExpanded(pageIndex, itemIndex) ? undefined : { pageIndex, itemIndex };
};
</script>
<!-- Item table -->
@ -77,13 +71,13 @@
<!-- Page items -->
{#each page.itemGroups as itemGroup, itemIdx}
<ItemRow {pageIdx} {itemIdx} {schema} {itemGroup} {changes} {maxPage} {pageIsPinned} />
<ItemRow pageIdx={page.index} {itemIdx} {schema} {itemGroup} {changes} {pageControl} />
{/each}
{/each}
</tbody>
</table>
{#if !pageIsPinned}
{#if !pagePinned}
{#if renderedMaxPage < pages.length}
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
<div class="my-6 text-center text-2xl">...</div>

View File

@ -0,0 +1,67 @@
import Page from '@core/debug/Page';
import StageResult from '@core/debug/StageResult';
import { Writable, writable, get, Readable, derived } from 'svelte/store';
export default class PageControl {
pinnedPageIndex: Writable<number | undefined>;
pagePinned: Readable<boolean>;
canPrev: Readable<boolean>;
canNext: Readable<boolean>;
pagesWithItems: Set<number> = new Set();
constructor(public totalPages: number) {
this.pinnedPageIndex = writable(undefined);
this.pagePinned = derived(this.pinnedPageIndex, (page) => !isNaN(page));
this.canPrev = derived([this.pinnedPageIndex, this.pagePinned], ([page, pinned]) => pinned && page > 0);
this.canNext = derived(
[this.pinnedPageIndex, this.pagePinned],
([page, pinned]) => pinned && page < totalPages - 1
);
this.next.bind(this);
this.prev.bind(this);
this.unpinPage.bind(this);
}
selectPages(stageResult: StageResult, relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number) {
const allRelevantPages = stageResult.selectPages(relevantChangesOnly, groupItems);
this.pagesWithItems = new Set(
allRelevantPages.filter((page) => page.itemGroups.length > 0).map((page) => page.index)
);
if (Number.isInteger(pinnedPage)) {
return allRelevantPages.filter((page) => page.index === pinnedPage);
}
return allRelevantPages;
}
pageHasItems(pageIdx: number) {
return this.pagesWithItems.has(pageIdx);
}
next() {
if (get(this.canNext)) {
this.pinnedPageIndex.update((page) => page + 1);
}
}
prev() {
if (get(this.canPrev)) {
this.pinnedPageIndex.update((page) => page - 1);
}
}
pinPage(pageIdx: number) {
this.pinnedPageIndex.set(pageIdx);
}
unpinPage() {
this.pinnedPageIndex.set(undefined);
}
pinnedPage() {
return get(this.pinnedPageIndex);
}
pageIsPinned() {
return get(this.pagePinned);
}
}

View File

@ -1,37 +1,39 @@
<script>
import { slide } from 'svelte/transition';
import { getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { Collection } from 'svelte-hero-icons';
import type { Writable } from 'svelte/store';
export let pagesNumbers: Set<number>;
export let maxPage: number;
export let pinnedPage: number;
import PageControl from './PageControl';
export let pageControl: PageControl;
let { pinnedPageIndex, pagePinned } = pageControl;
const popupOpened: Writable<boolean> = getContext('popupOpened');
const dispatch = createEventDispatcher();
function pinPage(index: number) {
popupOpened.set(false);
dispatch('pinPage', index);
pageControl.pinPage(index);
}
function unpinPage() {
popupOpened.set(false);
dispatch('unpinPage');
pageControl.unpinPage();
}
</script>
<div class="absolute mt-2 p-2 flex bg-gray-200 shadow-lg rounded-sm overflow-auto max-h-96" transition:slide>
<span class="mt-1 pr-2" on:click={!!pinnedPage && unpinPage}>
<Collection size="1x" class={!!pinnedPage ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
<span class="mt-1 pr-2" on:click={$pagePinned && unpinPage}>
<Collection size="1x" class={$pagePinned ? 'hover:text-select cursor-pointer' : 'opacity-50'} />
</span>
<div class="grid gap-3" style="grid-template-columns: repeat({Math.min(20, maxPage + 1)}, minmax(0, 1fr));">
{#each new Array(maxPage + 1) as _, idx}
<div
on:click={() => pagesNumbers.has(idx) && pinPage(idx)}
class="px-2 border border-gray-300 rounded-full text-center {pagesNumbers.has(idx) ? (pinnedPage === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}">
{idx}
class="grid gap-3"
style="grid-template-columns: repeat({Math.min(20, pageControl.totalPages)}, minmax(0, 1fr));">
{#each new Array(pageControl.totalPages) as _, idx}
<div
on:click={() => pageControl.pageHasItems(idx) && pinPage(idx)}
class="px-2 border border-gray-300 rounded-full text-center {pageControl.pageHasItems(idx) ? ($pinnedPageIndex === idx ? 'bg-select' : 'hover:text-select hover:border-select cursor-pointer') : 'opacity-50'}">
{idx + 1}
</div>
{/each}
</div>