Skip to content

Commit ad73dd9

Browse files
committed
feat: ToC style config
1 parent ab3a9ac commit ad73dd9

File tree

5 files changed

+220
-47
lines changed

5 files changed

+220
-47
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Add or edit PDF outlines / Table of contents in browser
1010

1111
- [x] Fix paging bug
1212
- [x] ToC with number order
13-
- [ ] ToC style config
13+
- [x] ToC style config
1414
- [x] AI prompt suggestion button
1515
- [ ] Analyse text to outline by Tab
1616
- [ ] Load outlines from PDF own

src/app.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
@import 'tailwindcss/utilities';
44

55
@layer components {
6+
7+
input {
8+
@apply outline-none;
9+
}
10+
611
.btn {
712
@apply px-4 py-2 rounded-md border border-black bg-white text-black text-sm transition duration-200
813
}
@@ -18,4 +23,4 @@
1823
.myfocus {
1924
@apply outline-none focus:outline-dashed focus:outline-gray-600
2025
}
21-
}
26+
}

src/lib/pdf-service.ts

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {get} from 'svelte/store';
22
import {PDFDocument, PDFName, PDFPage, PDFFont, rgb, StandardFonts} from 'pdf-lib';
33
import * as pdfjsLib from 'pdfjs-dist';
4-
import {showNumberedList} from '../stores';
4+
import {tocConfig} from '../stores';
55

66
export interface PDFState {
77
doc: PDFDocument | null;
@@ -82,6 +82,8 @@ export class PDFService {
8282
}
8383
) {
8484
let yOffset = startY;
85+
const config = get(tocConfig);
86+
const isFirstLevel = level === 0;
8587
const {regularFont, boldFont, pageWidth, pageHeight} = options;
8688
let {prefix} = options;
8789
let currentWorkingPage = currentPage;
@@ -90,53 +92,60 @@ export class PDFService {
9092
const item = items[i];
9193

9294
// Check if we need a new page
93-
// Add more space for margin at bottom
9495
if (yOffset < 60) {
9596
currentWorkingPage = doc.addPage([pageWidth, pageHeight]);
9697
yOffset = pageHeight - 60;
9798
}
9899

99-
const isFirstLevel = level === 0;
100-
const indent = level * 20;
101-
const fontSize = isFirstLevel ? 11 : 9;
102-
const font = isFirstLevel ? boldFont : regularFont;
103-
const color = isFirstLevel ? rgb(0, 0, 0) : rgb(0.3, 0.3, 0.3);
104-
const lineSpacing = fontSize + (isFirstLevel ? 8 : 6);
100+
// Fetch level-specific config
101+
const levelConfig = isFirstLevel ? config.firstLevel : config.otherLevels;
102+
const indentation = level * 20;
103+
const {fontSize, dotLeader, color, lineSpacing} = levelConfig;
104+
105+
const font = isFirstLevel ? options.boldFont : options.regularFont;
106+
const parsedColor = rgb(
107+
parseInt(color.slice(1, 3), 16) / 255,
108+
parseInt(color.slice(3, 5), 16) / 255,
109+
parseInt(color.slice(5, 7), 16) / 255
110+
);
111+
112+
const lineHeight = fontSize * lineSpacing;
105113

106114
// Calculate prefix
107-
const snl = get(showNumberedList);
115+
const snl = config.showNumberedList;
108116
const itemPrefix = snl ? (prefix ? `${prefix}.${i + 1}` : `${i + 1}`) : '';
109117
let title = `${itemPrefix} ${item.title}`;
110118

111119
// Draw title
112-
const titleX = 50 + indent;
120+
const titleX = 50 + indentation;
113121
if (isFirstLevel) {
114-
yOffset -= 6;
122+
yOffset -= 8;
115123
}
116-
117124
title = this.replaceUnsupportedCharacters(title, font);
118125
currentWorkingPage.drawText(title, {
119126
x: titleX,
120127
y: yOffset,
121128
size: fontSize,
122129
font,
123-
color,
124-
maxWidth: pageWidth - 100 - indent,
130+
color: parsedColor,
131+
maxWidth: pageWidth - 100 - indentation,
125132
});
126133

127134
// Draw dots
128-
const titleWidth = font.widthOfTextAtSize(title, fontSize);
129-
const dotsXStart = titleX + titleWidth + 10;
130-
const dotsXEnd = pageWidth - 65;
131-
132-
for (let x = dotsXStart; x < dotsXEnd; x += 5) {
133-
currentWorkingPage.drawText('.', {
134-
x,
135-
y: yOffset,
136-
size: fontSize * 0.6,
137-
font: regularFont,
138-
color: rgb(0.5, 0.5, 0.5),
139-
});
135+
if (dotLeader) {
136+
const titleWidth = font.widthOfTextAtSize(title, fontSize);
137+
const dotsXStart = titleX + titleWidth + 10;
138+
const dotsXEnd = pageWidth - 65;
139+
140+
for (let x = dotsXStart; x < dotsXEnd; x += 5) {
141+
currentWorkingPage.drawText(dotLeader, {
142+
x,
143+
y: yOffset,
144+
size: fontSize * 0.6,
145+
font: regularFont,
146+
color: parsedColor,
147+
});
148+
}
140149
}
141150

142151
// Draw page number
@@ -148,25 +157,23 @@ export class PDFService {
148157
y: yOffset,
149158
size: fontSize,
150159
font,
151-
color,
160+
color: parsedColor,
152161
});
153162

154163
// Create link annotation
155164
this.createLinkAnnotation(doc, currentWorkingPage, {
156-
pageNum: item.to,
165+
pageNum: item.to + config.pageOffset,
157166
pages,
158167
rect: [titleX, yOffset - 2, pageWidth - 50, yOffset + fontSize],
159168
});
160169

161-
yOffset -= lineSpacing;
170+
yOffset -= lineHeight;
162171

163172
if (item.children?.length) {
164-
// Pass the current item's prefix to children
165173
const childResult = await this.drawTocItems(doc, currentWorkingPage, item.children, pages, level + 1, yOffset, {
166174
...options,
167175
prefix: itemPrefix,
168176
});
169-
// Update current working page and yOffset from child results
170177
currentWorkingPage = childResult.currentPage;
171178
yOffset = childResult.yOffset;
172179
}
@@ -215,7 +222,9 @@ export class PDFService {
215222
const page = await pdf.getPage(pageNum);
216223
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
217224
const viewport = page.getViewport({scale});
218-
225+
if (!viewport) {
226+
return;
227+
}
219228
canvas.height = viewport.height;
220229
canvas.width = viewport.width;
221230

src/routes/+page.svelte

Lines changed: 142 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,30 @@
1010
1111
import {setOutline} from '../lib/pdf-outliner';
1212
import {PDFService, type PDFState} from '../lib/pdf-service';
13-
import {tocItems, pdfService, showNumberedList} from '../stores';
13+
import {tocItems, pdfService, type TocConfig} from '../stores';
1414
import {debounce} from '../lib';
1515
16+
import {tocConfig} from '../stores';
17+
18+
let config: TocConfig;
19+
tocConfig.subscribe((value) => (config = value));
20+
21+
function updateTocField(fieldPath, value) {
22+
tocConfig.update((cfg) => {
23+
const keys = fieldPath.split('.');
24+
let target = cfg;
25+
keys.slice(0, -1).forEach((key) => (target = target[key]));
26+
target[keys[keys.length - 1]] = value;
27+
return cfg;
28+
});
29+
}
30+
1631
let isDragging = false;
1732
1833
let pdfState: PDFState = {
1934
doc: null,
2035
newDoc: null,
36+
instance: null,
2137
filename: '',
2238
currentPage: 1,
2339
totalPages: 0,
@@ -58,7 +74,7 @@
5874
5975
const debouncedUpdatePDF = debounce(updatePDF, 300);
6076
tocItems.subscribe(debouncedUpdatePDF);
61-
showNumberedList.subscribe(debouncedUpdatePDF);
77+
tocConfig.subscribe(debouncedUpdatePDF);
6278
6379
const handleFileDrop = async (e: CustomEvent) => {
6480
const {acceptedFiles} = e.detail;
@@ -116,19 +132,134 @@
116132
<span class="text-3xl font-semibold">PDF Outliner</span>
117133
<Logo />
118134
</div>
119-
<div class="border-dashed border-gray-100 rounded border-2 my-4 p-2">
120-
<input
121-
bind:checked={$showNumberedList}
122-
type="checkbox"
123-
id="show_numbered_list"
124-
name="show_numbered_list"
125-
/>
126-
<label for="show_numbered_list">with numbered list</label>
135+
<div class="border-dashed border-gray-100 rounded border-2 p-2 my-4">
136+
<div class="border-dashed border-gray-100 rounded border-2 my-1 p-2">
137+
<input
138+
bind:checked={config.showNumberedList}
139+
type="checkbox"
140+
id="show_numbered_list"
141+
on:change={(e) => updateTocField('showNumberedList', e.target.checked)}
142+
/>
143+
<label for="show_numbered_list">with numbered list</label>
144+
</div>
145+
146+
<div class="border-dashed border-gray-100 rounded border-2 my-2 p-2 flex gap-6">
147+
<label
148+
class="whitespace-nowrap"
149+
for="page_offset">Page offset</label
150+
>
151+
<input
152+
type="number"
153+
id="page_offset"
154+
bind:value={config.pageOffset}
155+
on:input={(e) => updateTocField('pageOffset', parseInt(e.target.value, 10) || 0)}
156+
class=" w-[80%]"
157+
/>
158+
</div>
159+
160+
<div class="flex">
161+
<div class="w-36 inline-block mr-3">
162+
<h3 class="my-4 font-bold">First Level</h3>
163+
164+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2 mr-2 w-32">
165+
<label for="first_level_font_size">Font Size</label>
166+
<input
167+
type="number"
168+
id="first_level_font_size"
169+
bind:value={config.firstLevel.fontSize}
170+
on:input={(e) => updateTocField('firstLevel.fontSize', parseInt(e.target.value, 10) || 0)}
171+
class="w-[80%]"
172+
/>
173+
</div>
174+
175+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2 mr-2 w-32">
176+
<label for="first_level_dot_leader">Dot Leader</label>
177+
<input
178+
type="text"
179+
id="first_level_dot_leader"
180+
bind:value={config.firstLevel.dotLeader}
181+
on:input={(e) => updateTocField('firstLevel.dotLeader', e.target.value)}
182+
class=" w-[80%]"
183+
/>
184+
</div>
185+
186+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2 mr-2 w-32">
187+
<label for="first_level_color">Color</label>
188+
<input
189+
type="color"
190+
id="first_level_color"
191+
bind:value={config.firstLevel.color}
192+
on:input={(e) => updateTocField('firstLevel.color', e.target.value)}
193+
class=" w-[80%]"
194+
/>
195+
</div>
196+
197+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2 mr-2 w-32">
198+
<label for="first_level_line_spacing">Spacing</label>
199+
<input
200+
type="number"
201+
step="0.1"
202+
id="first_level_line_spacing"
203+
bind:value={config.firstLevel.lineSpacing}
204+
on:input={(e) => updateTocField('firstLevel.lineSpacing', parseFloat(e.target.value) || 1)}
205+
class=" w-[80%]"
206+
/>
207+
</div>
208+
</div>
209+
<div class="w-36 inline-block">
210+
<h3 class="my-4 font-bold">Other Levels</h3>
211+
212+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2">
213+
<label for="other_levels_font_size">Font Size</label>
214+
<input
215+
type="number"
216+
id="other_levels_font_size"
217+
bind:value={config.otherLevels.fontSize}
218+
on:input={(e) => updateTocField('otherLevels.fontSize', parseInt(e.target.value, 10) || 0)}
219+
class=" w-[80%]"
220+
/>
221+
</div>
222+
223+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2">
224+
<label for="other_levels_dot_leader">Dot Leader</label>
225+
<input
226+
type="text"
227+
id="other_levels_dot_leader"
228+
bind:value={config.otherLevels.dotLeader}
229+
on:input={(e) => updateTocField('otherLevels.dotLeader', e.target.value)}
230+
class=" w-[80%]"
231+
/>
232+
</div>
233+
234+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2">
235+
<label for="other_levels_color">Color</label>
236+
<input
237+
type="color"
238+
id="other_levels_color"
239+
bind:value={config.otherLevels.color}
240+
on:input={(e) => updateTocField('otherLevels.color', e.target.value)}
241+
class=" w-[80%]"
242+
/>
243+
</div>
244+
245+
<div class="border-dashed border-gray-100 rounded border-2 my-3 p-2">
246+
<label for="other_levels_line_spacing">Spacing</label>
247+
<input
248+
type="number"
249+
step="0.1"
250+
id="other_levels_line_spacing"
251+
bind:value={config.otherLevels.lineSpacing}
252+
on:input={(e) => updateTocField('otherLevels.lineSpacing', parseFloat(e.target.value) || 1)}
253+
class=" w-[80%]"
254+
/>
255+
</div>
256+
</div>
257+
</div>
127258
</div>
128259
<TocEditor />
129260
</div>
130261
<div class="flex flex-col flex-1">
131-
<div class="relative h-fit pb-8 min-h-[85vh]">
262+
<div class="h-fit pb-8 min-h-[85vh] top-5 sticky">
132263
<Dropzone
133264
containerClasses={pdfState.instance ? '' : 'h-full'}
134265
accept=".pdf"

src/stores.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1-
import { PDFService } from '$lib/pdf-service';
1+
import {PDFService} from '$lib/pdf-service';
22
import {writable} from 'svelte/store';
33

4+
export type StyleConfig = {
5+
fontSize: number;
6+
dotLeader: string;
7+
color: string;
8+
lineSpacing: string;
9+
};
10+
export type TocConfig = {
11+
showNumberedList: Boolean;
12+
pageOffset: number;
13+
firstLevel: StyleConfig;
14+
otherLevels: StyleConfig;
15+
};
16+
417
export const tocItems = writable([]);
518
export const maxPage = writable(0);
619
export const pdfService = writable(new PDFService());
7-
export const showNumberedList = writable(true);
20+
export const tocConfig = writable({
21+
showNumberedList: true,
22+
pageOffset: 0,
23+
firstLevel: {
24+
fontSize: 11,
25+
dotLeader: '.',
26+
color: '#000000',
27+
lineSpacing: 1.65,
28+
},
29+
otherLevels: {
30+
fontSize: 9,
31+
dotLeader: '',
32+
color: '#666666',
33+
lineSpacing: 1.5,
34+
},
35+
});

0 commit comments

Comments
 (0)