Skip to content

Commit 92220de

Browse files
committed
Refactor PDF text extraction to support configurable margins for header, footer, left, and right
1 parent f948601 commit 92220de

File tree

5 files changed

+200
-57
lines changed

5 files changed

+200
-57
lines changed

src/components/DocumentSettings.tsx

Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { Fragment, useState, useRef, useCallback, useEffect } from 'react';
3+
import { Fragment, useState, useRef, useEffect } from 'react';
44
import { Dialog, DialogPanel, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button } from '@headlessui/react';
55
import { useConfig, ViewType } from '@/contexts/ConfigContext';
66
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
@@ -21,32 +21,54 @@ const viewTypes = [
2121
];
2222

2323
export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsProps) {
24-
const { viewType, skipBlank, epubTheme, textExtractionMargin, updateConfigKey } = useConfig();
24+
const {
25+
viewType,
26+
skipBlank,
27+
epubTheme,
28+
headerMargin,
29+
footerMargin,
30+
leftMargin,
31+
rightMargin,
32+
updateConfigKey
33+
} = useConfig();
2534
const { createFullAudioBook } = useEPUB();
2635
const [progress, setProgress] = useState(0);
2736
const [isGenerating, setIsGenerating] = useState(false);
28-
const [localMargin, setLocalMargin] = useState(textExtractionMargin);
37+
const [localMargins, setLocalMargins] = useState({
38+
header: headerMargin,
39+
footer: footerMargin,
40+
left: leftMargin,
41+
right: rightMargin
42+
});
2943
const abortControllerRef = useRef<AbortController | null>(null);
3044
const selectedView = viewTypes.find(v => v.id === viewType) || viewTypes[0];
3145

32-
//console.log(localMargin, textExtractionMargin);
33-
34-
// Sync local margin with global state
46+
// Sync local margins with global state
3547
useEffect(() => {
36-
setLocalMargin(textExtractionMargin);
37-
}, [textExtractionMargin]);
48+
setLocalMargins({
49+
header: headerMargin,
50+
footer: footerMargin,
51+
left: leftMargin,
52+
right: rightMargin
53+
});
54+
}, [headerMargin, footerMargin, leftMargin, rightMargin]);
3855

3956
// Handler for slider change (updates local state only)
40-
const handleMarginChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
41-
setLocalMargin(Number(event.target.value));
42-
}, []);
57+
const handleMarginChange = (margin: keyof typeof localMargins) => (event: React.ChangeEvent<HTMLInputElement>) => {
58+
setLocalMargins(prev => ({
59+
...prev,
60+
[margin]: Number(event.target.value)
61+
}));
62+
};
4363

4464
// Handler for slider release
45-
const handleMarginChangeComplete = useCallback(() => {
46-
if (localMargin !== textExtractionMargin) {
47-
updateConfigKey('textExtractionMargin', localMargin);
65+
const handleMarginChangeComplete = (margin: keyof typeof localMargins) => () => {
66+
const value = localMargins[margin];
67+
const configKey = `${margin}Margin`;
68+
if (value !== (useConfig)[configKey as keyof typeof useConfig]) {
69+
updateConfigKey(configKey as 'headerMargin' | 'footerMargin' | 'leftMargin' | 'rightMargin', value);
4870
}
49-
}, [localMargin, textExtractionMargin, updateConfigKey]);
71+
};
5072

5173
const handleStartGeneration = async () => {
5274
setIsGenerating(true);
@@ -154,28 +176,92 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
154176
</div>}
155177
{!epub && <div className="space-y-6">
156178
<div className="mt-4 space-y-2">
157-
<label className="block text-sm font-medium text-foreground">
158-
Text Extraction Margin
179+
<label className="block text-sm font-medium text-foreground mb-4">
180+
Adjust extraction margins (experimental)
159181
</label>
160-
<div className="flex justify-between">
161-
<span className="text-xs">0%</span>
162-
<span className="text-xs font-bold">{Math.round(localMargin * 100)}%</span>
163-
<span className="text-xs">20%</span>
182+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
183+
{/* Header Margin */}
184+
<div className="space-y-1">
185+
<div className="flex justify-between">
186+
<span className="text-xs">Header</span>
187+
<span className="text-xs font-bold">{Math.round(localMargins.header * 100)}%</span>
188+
</div>
189+
<input
190+
type="range"
191+
min="0"
192+
max="0.2"
193+
step="0.01"
194+
value={localMargins.header}
195+
onChange={handleMarginChange('header')}
196+
onMouseUp={handleMarginChangeComplete('header')}
197+
onKeyUp={handleMarginChangeComplete('header')}
198+
onTouchEnd={handleMarginChangeComplete('header')}
199+
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
200+
/>
201+
</div>
202+
203+
{/* Footer Margin */}
204+
<div className="space-y-1">
205+
<div className="flex justify-between">
206+
<span className="text-xs">Footer</span>
207+
<span className="text-xs font-bold">{Math.round(localMargins.footer * 100)}%</span>
208+
</div>
209+
<input
210+
type="range"
211+
min="0"
212+
max="0.2"
213+
step="0.01"
214+
value={localMargins.footer}
215+
onChange={handleMarginChange('footer')}
216+
onMouseUp={handleMarginChangeComplete('footer')}
217+
onKeyUp={handleMarginChangeComplete('footer')}
218+
onTouchEnd={handleMarginChangeComplete('footer')}
219+
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
220+
/>
221+
</div>
222+
223+
{/* Left Margin */}
224+
<div className="space-y-1">
225+
<div className="flex justify-between">
226+
<span className="text-xs">Left</span>
227+
<span className="text-xs font-bold">{Math.round(localMargins.left * 100)}%</span>
228+
</div>
229+
<input
230+
type="range"
231+
min="0"
232+
max="0.2"
233+
step="0.01"
234+
value={localMargins.left}
235+
onChange={handleMarginChange('left')}
236+
onMouseUp={handleMarginChangeComplete('left')}
237+
onKeyUp={handleMarginChangeComplete('left')}
238+
onTouchEnd={handleMarginChangeComplete('left')}
239+
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
240+
/>
241+
</div>
242+
243+
{/* Right Margin */}
244+
<div className="space-y-1">
245+
<div className="flex justify-between">
246+
<span className="text-xs">Right</span>
247+
<span className="text-xs font-bold">{Math.round(localMargins.right * 100)}%</span>
248+
</div>
249+
<input
250+
type="range"
251+
min="0"
252+
max="0.2"
253+
step="0.01"
254+
value={localMargins.right}
255+
onChange={handleMarginChange('right')}
256+
onMouseUp={handleMarginChangeComplete('right')}
257+
onKeyUp={handleMarginChangeComplete('right')}
258+
onTouchEnd={handleMarginChangeComplete('right')}
259+
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
260+
/>
261+
</div>
164262
</div>
165-
<input
166-
type="range"
167-
min="0"
168-
max="0.2"
169-
step="0.01"
170-
value={localMargin}
171-
onChange={handleMarginChange}
172-
onMouseUp={handleMarginChangeComplete}
173-
onKeyUp={handleMarginChangeComplete}
174-
onTouchEnd={handleMarginChangeComplete}
175-
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
176-
/>
177-
<p className="text-xs text-muted">
178-
{"Don't"} include content from outer rim of the page during text extraction (experimental)
263+
<p className="text-xs text-muted mt-2">
264+
Adjust margins to exclude content from edges of the page during text extraction
179265
</p>
180266
</div>
181267
<Listbox

src/contexts/ConfigContext.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ type ConfigValues = {
1616
skipBlank: boolean;
1717
epubTheme: boolean;
1818
textExtractionMargin: number;
19+
headerMargin: number;
20+
footerMargin: number;
21+
leftMargin: number;
22+
rightMargin: number;
1923
};
2024

2125
/** Interface defining the configuration context shape and functionality */
@@ -28,6 +32,10 @@ interface ConfigContextType {
2832
skipBlank: boolean;
2933
epubTheme: boolean;
3034
textExtractionMargin: number;
35+
headerMargin: number;
36+
footerMargin: number;
37+
leftMargin: number;
38+
rightMargin: number;
3139
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
3240
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
3341
isLoading: boolean;
@@ -52,6 +60,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
5260
const [skipBlank, setSkipBlank] = useState<boolean>(true);
5361
const [epubTheme, setEpubTheme] = useState<boolean>(false);
5462
const [textExtractionMargin, setTextExtractionMargin] = useState<number>(0.07);
63+
const [headerMargin, setHeaderMargin] = useState<number>(0.07);
64+
const [footerMargin, setFooterMargin] = useState<number>(0.07);
65+
const [leftMargin, setLeftMargin] = useState<number>(0.07);
66+
const [rightMargin, setRightMargin] = useState<number>(0.07);
5567

5668
const [isLoading, setIsLoading] = useState(true);
5769
const [isDBReady, setIsDBReady] = useState(false);
@@ -72,6 +84,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
7284
const cachedSkipBlank = await getItem('skipBlank');
7385
const cachedEpubTheme = await getItem('epubTheme');
7486
const cachedMargin = await getItem('textExtractionMargin');
87+
const cachedHeaderMargin = await getItem('headerMargin');
88+
const cachedFooterMargin = await getItem('footerMargin');
89+
const cachedLeftMargin = await getItem('leftMargin');
90+
const cachedRightMargin = await getItem('rightMargin');
7591

7692
// Only set API key and base URL if they were explicitly saved by the user
7793
if (cachedApiKey) {
@@ -90,6 +106,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
90106
setSkipBlank(cachedSkipBlank === 'false' ? false : true);
91107
setEpubTheme(cachedEpubTheme === 'true');
92108
setTextExtractionMargin(parseFloat(cachedMargin || '0.07'));
109+
setHeaderMargin(parseFloat(cachedHeaderMargin || '0.07'));
110+
setFooterMargin(parseFloat(cachedFooterMargin || '0.07'));
111+
setLeftMargin(parseFloat(cachedLeftMargin || '0.07'));
112+
setRightMargin(parseFloat(cachedRightMargin || '0.07'));
93113

94114
// Only save non-sensitive settings by default
95115
if (!cachedViewType) {
@@ -104,6 +124,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
104124
if (cachedMargin === null) {
105125
await setItem('textExtractionMargin', '0.07');
106126
}
127+
if (cachedHeaderMargin === null) await setItem('headerMargin', '0.07');
128+
if (cachedFooterMargin === null) await setItem('footerMargin', '0.07');
129+
if (cachedLeftMargin === null) await setItem('leftMargin', '0.0');
130+
if (cachedRightMargin === null) await setItem('rightMargin', '0.0');
107131

108132
} catch (error) {
109133
console.error('Error initializing:', error);
@@ -181,6 +205,18 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
181205
case 'textExtractionMargin':
182206
setTextExtractionMargin(value as number);
183207
break;
208+
case 'headerMargin':
209+
setHeaderMargin(value as number);
210+
break;
211+
case 'footerMargin':
212+
setFooterMargin(value as number);
213+
break;
214+
case 'leftMargin':
215+
setLeftMargin(value as number);
216+
break;
217+
case 'rightMargin':
218+
setRightMargin(value as number);
219+
break;
184220
}
185221
} catch (error) {
186222
console.error(`Error updating config key ${key}:`, error);
@@ -198,6 +234,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
198234
skipBlank,
199235
epubTheme,
200236
textExtractionMargin,
237+
headerMargin,
238+
footerMargin,
239+
leftMargin,
240+
rightMargin,
201241
updateConfig,
202242
updateConfigKey,
203243
isLoading,

src/contexts/EPUBContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
233233

234234
audioChunks.push(audioBuffer);
235235

236-
// Add a small pause between sections (500ms of silence)
237-
const silenceBuffer = new ArrayBuffer(24000);
236+
// Add a small pause between sections (1s of silence)
237+
const silenceBuffer = new ArrayBuffer(48000);
238238
audioChunks.push(silenceBuffer);
239239

240240
} catch (error) {

src/contexts/PDFContext.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,19 @@ const PDFContext = createContext<PDFContextType | undefined>(undefined);
7777
* @param {ReactNode} props.children - Child components to be wrapped by the provider
7878
*/
7979
export function PDFProvider({ children }: { children: ReactNode }) {
80-
const { setText: setTTSText, stop, currDocPageNumber: currDocPage, currDocPages, setCurrDocPages } = useTTS();
81-
const { textExtractionMargin } = useConfig();
80+
const {
81+
setText: setTTSText,
82+
stop,
83+
currDocPageNumber: currDocPage,
84+
currDocPages,
85+
setCurrDocPages
86+
} = useTTS();
87+
const {
88+
headerMargin,
89+
footerMargin,
90+
leftMargin,
91+
rightMargin
92+
} = useConfig();
8293

8394
// Current document state
8495
const [currDocURL, setCurrDocURL] = useState<string>();
@@ -106,7 +117,12 @@ export function PDFProvider({ children }: { children: ReactNode }) {
106117
const loadCurrDocText = useCallback(async () => {
107118
try {
108119
if (!pdfDocument) return;
109-
const text = await extractTextFromPDF(pdfDocument, currDocPage, textExtractionMargin);
120+
const text = await extractTextFromPDF(pdfDocument, currDocPage, {
121+
header: headerMargin,
122+
footer: footerMargin,
123+
left: leftMargin,
124+
right: rightMargin
125+
});
110126
// Only update TTS text if the content has actually changed
111127
// This prevents unnecessary resets of the sentence index
112128
if (text !== currDocText || text === '') {
@@ -116,7 +132,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
116132
} catch (error) {
117133
console.error('Error loading PDF text:', error);
118134
}
119-
}, [pdfDocument, currDocPage, setTTSText, currDocText, textExtractionMargin]);
135+
}, [pdfDocument, currDocPage, setTTSText, currDocText, headerMargin, footerMargin, leftMargin, rightMargin]);
120136

121137
/**
122138
* Effect hook to update document text when the page changes

0 commit comments

Comments
 (0)