mevebu 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/mevebu/app/tailwind.css +85 -0
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/files.js +125 -0
- package/mevebu/libs/js/icons.js +2 -1
- package/mevebu/libs/js/pdf-engine.js +67 -0
- package/mevebu/libs/js/registry.js +180 -0
- package/mevebu/libs/js/tools/audio-converter.js +228 -0
- package/mevebu/libs/js/tools/image-to-pdf.js +251 -0
- package/mevebu/libs/js/tools/pdf-merge.js +158 -0
- package/mevebu/libs/js/tools/pdf-organizer.js +215 -0
- package/mevebu/libs/js/tools/pdf-split.js +198 -0
- package/mevebu/libs/js/tools/pdf-to-image.js +190 -0
- package/mevebu/libs/js/tools/pdf-to-text.js +172 -0
- package/mevebu/libs/js/tools/svg-to-png.js +207 -0
- package/mevebu/libs/js/tools/text-to-pdf.js +182 -0
- package/mevebu/libs/js/tools/video-converter.js +316 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# mevebu
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**198 free browser tools, one page each. No sign up. No uploads. Everything runs in your tab.**
|
|
6
6
|
|
|
7
7
|
A private toolbox for the small jobs that do not deserve a whole app: format JSON, decode a JWT, make a password, build a cron line, pick a color, scan a QR code, and a lot more.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ A private toolbox for the small jobs that do not deserve a whole app: format JSO
|
|
|
10
10
|
[](https://www.npmjs.com/package/mevebu)
|
|
11
11
|
[](./LICENSE)
|
|
12
12
|
[](https://github.com/mevebu/mevebu/stargazers)
|
|
13
|
-

|
|
14
14
|

|
|
15
15
|
|
|
16
16
|
[Quick start](#quick-start) · [What's inside](#whats-inside) · [Privacy](#privacy-by-design) · [Host it yourself](#host-it-yourself) · [Development](#development) · [FAQ](#faq)
|
|
@@ -23,7 +23,7 @@ A private toolbox for the small jobs that do not deserve a whole app: format JSO
|
|
|
23
23
|
|
|
24
24
|
## What is mevebu?
|
|
25
25
|
|
|
26
|
-
mevebu is a collection of **
|
|
26
|
+
mevebu is a collection of **198 small online tools** that all run **inside your browser**. Each tool lives on its own page, does one job, and does it fast. Most of them update as you type, so there is no submit button and no waiting.
|
|
27
27
|
|
|
28
28
|
Think of it as a clean, private alternative to the usual "free online tools" websites. There are no accounts, no ads in your way, no uploads, and no tracking. Your text, passwords, tokens, and files stay on your device.
|
|
29
29
|
|
|
@@ -65,14 +65,14 @@ mevebu tries port `5555` first, then `5678`, then `5432`, and finally any free p
|
|
|
65
65
|
|
|
66
66
|
## What's inside
|
|
67
67
|
|
|
68
|
-
mevebu has **
|
|
68
|
+
mevebu has **198 tools in 20 categories**. Here is the full breakdown:
|
|
69
69
|
|
|
70
70
|
| Category | Tools |
|
|
71
71
|
| ------------------ | ----: |
|
|
72
72
|
| Random | 3 |
|
|
73
73
|
| Passwords and keys | 10 |
|
|
74
74
|
| Developer | 21 |
|
|
75
|
-
| Convert |
|
|
75
|
+
| Convert | 15 |
|
|
76
76
|
| Text and data | 14 |
|
|
77
77
|
| Daily use | 8 |
|
|
78
78
|
| Math | 17 |
|
|
@@ -89,7 +89,7 @@ mevebu has **188 tools in 20 categories**. Here is the full breakdown:
|
|
|
89
89
|
| GitHub | 8 |
|
|
90
90
|
| Units | 8 |
|
|
91
91
|
| QR codes | 2 |
|
|
92
|
-
| **Total** | **
|
|
92
|
+
| **Total** | **198** |
|
|
93
93
|
|
|
94
94
|
The counts come straight from the tool registry, so this table and the app never drift apart.
|
|
95
95
|
|
|
@@ -180,7 +180,7 @@ That is all. The catalog, search, breadcrumbs, and category pages pick it up aut
|
|
|
180
180
|
## FAQ
|
|
181
181
|
|
|
182
182
|
**What is mevebu?**
|
|
183
|
-
mevebu is a free, open source collection of
|
|
183
|
+
mevebu is a free, open source collection of 198 browser based tools for developers, students, creators, and everyday use. Every tool runs locally in your browser.
|
|
184
184
|
|
|
185
185
|
**Is mevebu really free?**
|
|
186
186
|
Yes. It is MIT licensed, with no paid tier, no account, and no usage limits.
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -218,6 +218,9 @@
|
|
|
218
218
|
.inset-0 {
|
|
219
219
|
inset: 0px;
|
|
220
220
|
}
|
|
221
|
+
.start-1 {
|
|
222
|
+
inset-inline-start: calc(var(--spacing) * 1);
|
|
223
|
+
}
|
|
221
224
|
.end-1 {
|
|
222
225
|
inset-inline-end: calc(var(--spacing) * 1);
|
|
223
226
|
}
|
|
@@ -421,6 +424,10 @@
|
|
|
421
424
|
width: calc(var(--spacing) * 6);
|
|
422
425
|
height: calc(var(--spacing) * 6);
|
|
423
426
|
}
|
|
427
|
+
.size-7 {
|
|
428
|
+
width: calc(var(--spacing) * 7);
|
|
429
|
+
height: calc(var(--spacing) * 7);
|
|
430
|
+
}
|
|
424
431
|
.size-8 {
|
|
425
432
|
width: calc(var(--spacing) * 8);
|
|
426
433
|
height: calc(var(--spacing) * 8);
|
|
@@ -433,6 +440,10 @@
|
|
|
433
440
|
width: calc(var(--spacing) * 11);
|
|
434
441
|
height: calc(var(--spacing) * 11);
|
|
435
442
|
}
|
|
443
|
+
.size-12 {
|
|
444
|
+
width: calc(var(--spacing) * 12);
|
|
445
|
+
height: calc(var(--spacing) * 12);
|
|
446
|
+
}
|
|
436
447
|
.size-14 {
|
|
437
448
|
width: calc(var(--spacing) * 14);
|
|
438
449
|
height: calc(var(--spacing) * 14);
|
|
@@ -495,6 +506,9 @@
|
|
|
495
506
|
.h-14 {
|
|
496
507
|
height: calc(var(--spacing) * 14);
|
|
497
508
|
}
|
|
509
|
+
.h-24 {
|
|
510
|
+
height: calc(var(--spacing) * 24);
|
|
511
|
+
}
|
|
498
512
|
.h-\[22px\] {
|
|
499
513
|
height: 22px;
|
|
500
514
|
}
|
|
@@ -525,6 +539,9 @@
|
|
|
525
539
|
.max-h-80 {
|
|
526
540
|
max-height: calc(var(--spacing) * 80);
|
|
527
541
|
}
|
|
542
|
+
.max-h-\[320px\] {
|
|
543
|
+
max-height: 320px;
|
|
544
|
+
}
|
|
528
545
|
.max-h-\[340px\] {
|
|
529
546
|
max-height: 340px;
|
|
530
547
|
}
|
|
@@ -603,6 +620,9 @@
|
|
|
603
620
|
.w-\[min\(640px\,100\%\)\] {
|
|
604
621
|
width: min(640px, 100%);
|
|
605
622
|
}
|
|
623
|
+
.w-auto {
|
|
624
|
+
width: auto;
|
|
625
|
+
}
|
|
606
626
|
.w-full {
|
|
607
627
|
width: 100%;
|
|
608
628
|
}
|
|
@@ -642,6 +662,9 @@
|
|
|
642
662
|
.max-w-\[var\(--bench\)\] {
|
|
643
663
|
max-width: var(--bench);
|
|
644
664
|
}
|
|
665
|
+
.max-w-full {
|
|
666
|
+
max-width: 100%;
|
|
667
|
+
}
|
|
645
668
|
.min-w-0 {
|
|
646
669
|
min-width: 0px;
|
|
647
670
|
}
|
|
@@ -657,6 +680,9 @@
|
|
|
657
680
|
.shrink {
|
|
658
681
|
flex-shrink: 1;
|
|
659
682
|
}
|
|
683
|
+
.grow {
|
|
684
|
+
flex-grow: 1;
|
|
685
|
+
}
|
|
660
686
|
.border-collapse {
|
|
661
687
|
border-collapse: collapse;
|
|
662
688
|
}
|
|
@@ -675,6 +701,15 @@
|
|
|
675
701
|
.scale-\[0\.97\] {
|
|
676
702
|
scale: 0.97;
|
|
677
703
|
}
|
|
704
|
+
.-rotate-90 {
|
|
705
|
+
rotate: calc(90deg * -1);
|
|
706
|
+
}
|
|
707
|
+
.rotate-90 {
|
|
708
|
+
rotate: 90deg;
|
|
709
|
+
}
|
|
710
|
+
.rotate-180 {
|
|
711
|
+
rotate: 180deg;
|
|
712
|
+
}
|
|
678
713
|
.transform {
|
|
679
714
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
|
680
715
|
}
|
|
@@ -705,12 +740,21 @@
|
|
|
705
740
|
.\[grid-template-columns\:minmax\(90px\,34\%\)_minmax\(0\,1fr\)\] {
|
|
706
741
|
grid-template-columns: minmax(90px,34%) minmax(0,1fr);
|
|
707
742
|
}
|
|
743
|
+
.\[grid-template-columns\:repeat\(96\,minmax\(0\,1fr\)\)\] {
|
|
744
|
+
grid-template-columns: repeat(96,minmax(0,1fr));
|
|
745
|
+
}
|
|
708
746
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(92px\,1fr\)\)\] {
|
|
709
747
|
grid-template-columns: repeat(auto-fill,minmax(92px,1fr));
|
|
710
748
|
}
|
|
749
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(150px\,1fr\)\)\] {
|
|
750
|
+
grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
|
|
751
|
+
}
|
|
711
752
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(160px\,1fr\)\)\] {
|
|
712
753
|
grid-template-columns: repeat(auto-fill,minmax(160px,1fr));
|
|
713
754
|
}
|
|
755
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(170px\,1fr\)\)\] {
|
|
756
|
+
grid-template-columns: repeat(auto-fill,minmax(170px,1fr));
|
|
757
|
+
}
|
|
714
758
|
.\[grid-template-columns\:repeat\(auto-fit\,minmax\(96px\,1fr\)\)\] {
|
|
715
759
|
grid-template-columns: repeat(auto-fit,minmax(96px,1fr));
|
|
716
760
|
}
|
|
@@ -750,6 +794,9 @@
|
|
|
750
794
|
.items-center {
|
|
751
795
|
align-items: center;
|
|
752
796
|
}
|
|
797
|
+
.items-end {
|
|
798
|
+
align-items: flex-end;
|
|
799
|
+
}
|
|
753
800
|
.items-start {
|
|
754
801
|
align-items: flex-start;
|
|
755
802
|
}
|
|
@@ -851,6 +898,9 @@
|
|
|
851
898
|
.rounded {
|
|
852
899
|
border-radius: 0.25rem;
|
|
853
900
|
}
|
|
901
|
+
.rounded-\[1px\] {
|
|
902
|
+
border-radius: 1px;
|
|
903
|
+
}
|
|
854
904
|
.rounded-\[3px\] {
|
|
855
905
|
border-radius: 3px;
|
|
856
906
|
}
|
|
@@ -973,6 +1023,12 @@
|
|
|
973
1023
|
.bg-jade-soft {
|
|
974
1024
|
background-color: var(--jade-soft);
|
|
975
1025
|
}
|
|
1026
|
+
.bg-jade\/70 {
|
|
1027
|
+
background-color: var(--jade);
|
|
1028
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1029
|
+
background-color: color-mix(in oklab, var(--jade) 70%, transparent);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
976
1032
|
.bg-orbit {
|
|
977
1033
|
background-color: var(--orbit);
|
|
978
1034
|
}
|
|
@@ -997,9 +1053,15 @@
|
|
|
997
1053
|
.bg-\[image\:var\(--caret\)\] {
|
|
998
1054
|
background-image: var(--caret);
|
|
999
1055
|
}
|
|
1056
|
+
.bg-\[repeating-conic-gradient\(\#0000_0\%_25\%\,var\(--deck-2\)_0\%_50\%\)\] {
|
|
1057
|
+
background-image: repeating-conic-gradient(#0000 0% 25%,var(--deck-2) 0% 50%);
|
|
1058
|
+
}
|
|
1000
1059
|
.bg-\[length\:14px\] {
|
|
1001
1060
|
background-size: 14px;
|
|
1002
1061
|
}
|
|
1062
|
+
.bg-\[length\:16px_16px\] {
|
|
1063
|
+
background-size: 16px 16px;
|
|
1064
|
+
}
|
|
1003
1065
|
.bg-\[position\:right_12px_center\] {
|
|
1004
1066
|
background-position: right 12px center;
|
|
1005
1067
|
}
|
|
@@ -1009,6 +1071,9 @@
|
|
|
1009
1071
|
.object-contain {
|
|
1010
1072
|
object-fit: contain;
|
|
1011
1073
|
}
|
|
1074
|
+
.object-cover {
|
|
1075
|
+
object-fit: cover;
|
|
1076
|
+
}
|
|
1012
1077
|
.p-0 {
|
|
1013
1078
|
padding: 0px;
|
|
1014
1079
|
}
|
|
@@ -1027,6 +1092,9 @@
|
|
|
1027
1092
|
.p-2\.5 {
|
|
1028
1093
|
padding: calc(var(--spacing) * 2.5);
|
|
1029
1094
|
}
|
|
1095
|
+
.p-3 {
|
|
1096
|
+
padding: calc(var(--spacing) * 3);
|
|
1097
|
+
}
|
|
1030
1098
|
.p-3\.5 {
|
|
1031
1099
|
padding: calc(var(--spacing) * 3.5);
|
|
1032
1100
|
}
|
|
@@ -1060,6 +1128,9 @@
|
|
|
1060
1128
|
.px-1 {
|
|
1061
1129
|
padding-inline: var(--spacing);
|
|
1062
1130
|
}
|
|
1131
|
+
.px-1\.5 {
|
|
1132
|
+
padding-inline: calc(var(--spacing) * 1.5);
|
|
1133
|
+
}
|
|
1063
1134
|
.px-2 {
|
|
1064
1135
|
padding-inline: calc(var(--spacing) * 2);
|
|
1065
1136
|
}
|
|
@@ -1096,6 +1167,9 @@
|
|
|
1096
1167
|
.px-\[var\(--margin\)\] {
|
|
1097
1168
|
padding-inline: var(--margin);
|
|
1098
1169
|
}
|
|
1170
|
+
.py-0\.5 {
|
|
1171
|
+
padding-block: calc(var(--spacing) * 0.5);
|
|
1172
|
+
}
|
|
1099
1173
|
.py-1 {
|
|
1100
1174
|
padding-block: var(--spacing);
|
|
1101
1175
|
}
|
|
@@ -1516,6 +1590,11 @@
|
|
|
1516
1590
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
1517
1591
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
1518
1592
|
}
|
|
1593
|
+
.transition-\[width\] {
|
|
1594
|
+
transition-property: width;
|
|
1595
|
+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
1596
|
+
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
1597
|
+
}
|
|
1519
1598
|
.transition-colors {
|
|
1520
1599
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
|
1521
1600
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
@@ -1704,9 +1783,15 @@
|
|
|
1704
1783
|
.disabled\:cursor-not-allowed:disabled {
|
|
1705
1784
|
cursor: not-allowed;
|
|
1706
1785
|
}
|
|
1786
|
+
.disabled\:opacity-40:disabled {
|
|
1787
|
+
opacity: 40%;
|
|
1788
|
+
}
|
|
1707
1789
|
.disabled\:opacity-45:disabled {
|
|
1708
1790
|
opacity: 45%;
|
|
1709
1791
|
}
|
|
1792
|
+
.disabled\:opacity-50:disabled {
|
|
1793
|
+
opacity: 50%;
|
|
1794
|
+
}
|
|
1710
1795
|
.has-checked\:border-jade:has(:checked) {
|
|
1711
1796
|
border-color: var(--jade);
|
|
1712
1797
|
}
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -750,7 +750,7 @@ export function blank(iconName, text) {
|
|
|
750
750
|
// ---------- version ----------
|
|
751
751
|
|
|
752
752
|
// Kept in one place. The CLI reads the same value from package.json.
|
|
753
|
-
export const VERSION = "1.
|
|
753
|
+
export const VERSION = "1.11.0";
|
|
754
754
|
export const PACKAGE_NAME = "mevebu";
|
|
755
755
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
756
756
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// File plumbing shared by the converter tools: a drop target, a size label, a
|
|
2
|
+
// name without its extension, and an image decoder. Kept here so ten tools do
|
|
3
|
+
// not each grow their own copy.
|
|
4
|
+
import { el, icon } from "./core.js";
|
|
5
|
+
|
|
6
|
+
export function formatBytes(bytes) {
|
|
7
|
+
if (!bytes) return "0 B";
|
|
8
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10240 ? 1 : 0)} KB`;
|
|
10
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function baseName(name) {
|
|
14
|
+
return (name || "file").replace(/\.[^.]+$/, "") || "file";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function extension(name) {
|
|
18
|
+
const match = /\.([^.]+)$/.exec(name || "");
|
|
19
|
+
return match ? match[1].toLowerCase() : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A dashed drop zone. Calls `onFiles` with a FileList or an array of files. */
|
|
23
|
+
export function dropzone({ label, hint = "or choose a file", accept = "", multiple = false, iconName = "image-up" }) {
|
|
24
|
+
const picker = el("input", { type: "file", accept, multiple, class: "hidden" });
|
|
25
|
+
const node = el(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
class:
|
|
29
|
+
"grid cursor-pointer justify-items-center gap-1.5 rounded-r2 border border-dashed border-orbit-hard bg-deck-2 px-4 py-7 text-center transition-[border-color,background-color] duration-150 hover:border-jade focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-jade data-[drag]:border-jade data-[drag]:bg-jade-soft",
|
|
30
|
+
role: "button",
|
|
31
|
+
tabindex: "0",
|
|
32
|
+
"aria-label": label,
|
|
33
|
+
},
|
|
34
|
+
icon(iconName, "size-6 text-ink-3"),
|
|
35
|
+
el("p", { class: "text-sm font-semibold" }, label),
|
|
36
|
+
el("p", { class: "text-2xs text-ink-3" }, hint),
|
|
37
|
+
picker
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
let handler = () => {};
|
|
41
|
+
let depth = 0;
|
|
42
|
+
|
|
43
|
+
node.addEventListener("click", () => picker.click());
|
|
44
|
+
node.addEventListener("keydown", (event) => {
|
|
45
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
picker.click();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
picker.addEventListener("change", () => {
|
|
51
|
+
if (picker.files?.length) handler(picker.files);
|
|
52
|
+
picker.value = "";
|
|
53
|
+
});
|
|
54
|
+
node.addEventListener("dragenter", (event) => {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
depth++;
|
|
57
|
+
node.dataset.drag = "";
|
|
58
|
+
});
|
|
59
|
+
node.addEventListener("dragover", (event) => event.preventDefault());
|
|
60
|
+
node.addEventListener("dragleave", () => {
|
|
61
|
+
depth = Math.max(0, depth - 1);
|
|
62
|
+
if (!depth) delete node.dataset.drag;
|
|
63
|
+
});
|
|
64
|
+
node.addEventListener("drop", (event) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
depth = 0;
|
|
67
|
+
delete node.dataset.drag;
|
|
68
|
+
const files = Array.from(event.dataTransfer?.files ?? []);
|
|
69
|
+
if (files.length) handler(files);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
node.onFiles = (fn) => {
|
|
73
|
+
handler = fn;
|
|
74
|
+
};
|
|
75
|
+
node.picker = picker;
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Decodes an image file, preferring createImageBitmap and falling back to <img>. */
|
|
80
|
+
export async function decodeImage(file) {
|
|
81
|
+
if (typeof createImageBitmap === "function") {
|
|
82
|
+
try {
|
|
83
|
+
return await createImageBitmap(file, { imageOrientation: "from-image" });
|
|
84
|
+
} catch {
|
|
85
|
+
// SVG and a few odd files only decode through an <img>.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const url = URL.createObjectURL(file);
|
|
89
|
+
try {
|
|
90
|
+
const image = new Image();
|
|
91
|
+
image.decoding = "sync";
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
image.onload = resolve;
|
|
94
|
+
image.onerror = () => reject(new Error("This file is not an image the browser can read."));
|
|
95
|
+
image.src = url;
|
|
96
|
+
});
|
|
97
|
+
return image;
|
|
98
|
+
} finally {
|
|
99
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function imageSize(source) {
|
|
104
|
+
return {
|
|
105
|
+
width: source.width || source.naturalWidth || 0,
|
|
106
|
+
height: source.height || source.naturalHeight || 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function canvasToBlob(canvas, type = "image/png", quality) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
canvas.toBlob(
|
|
113
|
+
(blob) => (blob ? resolve(blob) : reject(new Error("The browser could not write this file."))),
|
|
114
|
+
type,
|
|
115
|
+
quality
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Sorts files by name with numbers read as numbers, so page-2 comes before page-10. */
|
|
121
|
+
export function naturalSort(files) {
|
|
122
|
+
return [...files].sort((a, b) =>
|
|
123
|
+
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" })
|
|
124
|
+
);
|
|
125
|
+
}
|
package/mevebu/libs/js/icons.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED FILE, DO NOT EDIT.
|
|
2
|
-
// Source: lucide v1.57.0 (ISC), icons/
|
|
2
|
+
// Source: lucide v1.57.0 (ISC), icons/175 names from scripts/vendor-icons.mjs
|
|
3
3
|
// Regenerate with: npm run icons
|
|
4
4
|
|
|
5
5
|
const D = {
|
|
@@ -45,6 +45,7 @@ const D = {
|
|
|
45
45
|
"download": [ ["path", { d: "M12 15V3" }], ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }], ["path", { d: "m7 10 5 5 5-5" }] ],
|
|
46
46
|
"upload": [ ["path", { d: "M12 3v12" }], ["path", { d: "m17 8-5-5-5 5" }], ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }] ],
|
|
47
47
|
"delete": [ [ "path", { d: "M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z" } ], ["path", { d: "m12 9 6 6" }], ["path", { d: "m18 9-6 6" }] ],
|
|
48
|
+
"scissors": [ ["circle", { cx: "6", cy: "6", r: "3" }], ["path", { d: "M8.12 8.12 12 12" }], ["path", { d: "M20 4 8.12 15.88" }], ["circle", { cx: "6", cy: "18", r: "3" }], ["path", { d: "M14.8 14.8 20 20" }] ],
|
|
48
49
|
"arrow-left-right": [ ["path", { d: "M8 3 4 7l4 4" }], ["path", { d: "M4 7h16" }], ["path", { d: "m16 21 4-4-4-4" }], ["path", { d: "M20 17H4" }] ],
|
|
49
50
|
"trash": [ ["path", { d: "M10 11v6" }], ["path", { d: "M14 11v6" }], ["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }], ["path", { d: "M3 6h18" }], ["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }] ],
|
|
50
51
|
"plus": [ ["path", { d: "M5 12h14" }], ["path", { d: "M12 5v14" }] ],
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Loaders for the two vendored PDF libraries. Each one is fetched the first time
|
|
2
|
+
// a tool needs it, so opening the index never downloads a PDF engine.
|
|
3
|
+
const PDFJS_URL = new URL("./vendor/pdf.min.mjs", import.meta.url).href;
|
|
4
|
+
const WORKER_URL = new URL("./vendor/pdf.worker.min.mjs", import.meta.url).href;
|
|
5
|
+
const PDFLIB_URL = new URL("./vendor/pdf-lib.min.js", import.meta.url).href;
|
|
6
|
+
|
|
7
|
+
let pdfjsPromise = null;
|
|
8
|
+
let pdfLibPromise = null;
|
|
9
|
+
|
|
10
|
+
/** pdf.js, configured to run its worker from this folder, never from a CDN. */
|
|
11
|
+
export function loadPdfjs() {
|
|
12
|
+
if (!pdfjsPromise) {
|
|
13
|
+
pdfjsPromise = import(PDFJS_URL).then((module) => {
|
|
14
|
+
module.GlobalWorkerOptions.workerSrc = WORKER_URL;
|
|
15
|
+
return module;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return pdfjsPromise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** pdf-lib, which ships as a UMD bundle and lands on `globalThis.PDFLib`. */
|
|
22
|
+
export function loadPdfLib() {
|
|
23
|
+
if (globalThis.PDFLib) return Promise.resolve(globalThis.PDFLib);
|
|
24
|
+
if (!pdfLibPromise) {
|
|
25
|
+
pdfLibPromise = new Promise((resolve, reject) => {
|
|
26
|
+
const script = document.createElement("script");
|
|
27
|
+
script.src = PDFLIB_URL;
|
|
28
|
+
script.onload = () =>
|
|
29
|
+
globalThis.PDFLib ? resolve(globalThis.PDFLib) : reject(new Error("The PDF writer did not start."));
|
|
30
|
+
script.onerror = () => reject(new Error("The PDF writer could not be loaded."));
|
|
31
|
+
document.head.append(script);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return pdfLibPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function readPdfjs(file) {
|
|
38
|
+
const pdfjs = await loadPdfjs();
|
|
39
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
40
|
+
return pdfjs.getDocument({ data: bytes, isEvalSupported: false }).promise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function readPdfLib(file) {
|
|
44
|
+
const { PDFDocument } = await loadPdfLib();
|
|
45
|
+
return PDFDocument.load(await file.arrayBuffer(), { ignoreEncryption: false });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** "1-3, 7, 10-" against a page count, returning zero based indexes. */
|
|
49
|
+
export function parsePageRange(text, count) {
|
|
50
|
+
const clean = String(text || "").trim();
|
|
51
|
+
if (!clean) return Array.from({ length: count }, (_, i) => i);
|
|
52
|
+
const pages = new Set();
|
|
53
|
+
for (const part of clean.split(",")) {
|
|
54
|
+
const piece = part.trim();
|
|
55
|
+
if (!piece) continue;
|
|
56
|
+
const match = /^(\d+)?\s*(?:-\s*(\d+)?)?$/.exec(piece);
|
|
57
|
+
if (!match) throw new Error(`"${piece}" is not a page or a range.`);
|
|
58
|
+
const from = match[1] ? Number(match[1]) : 1;
|
|
59
|
+
const to = match[2] ? Number(match[2]) : match[1] ? from : count;
|
|
60
|
+
if (from < 1 || to < 1) throw new Error("Pages start at 1.");
|
|
61
|
+
if (from > count || to > count) throw new Error(`This file has ${count} page${count === 1 ? "" : "s"}.`);
|
|
62
|
+
if (from > to) throw new Error(`"${piece}" runs backwards.`);
|
|
63
|
+
for (let page = from; page <= to; page++) pages.add(page - 1);
|
|
64
|
+
}
|
|
65
|
+
if (!pages.size) throw new Error("That range selects no pages.");
|
|
66
|
+
return [...pages].sort((a, b) => a - b);
|
|
67
|
+
}
|
|
@@ -395,6 +395,186 @@ export const TOOLS = [
|
|
|
395
395
|
keywords: "favicon ico png apple touch icon android manifest icon generator",
|
|
396
396
|
load: () => import("./tools/favicon-converter.js"),
|
|
397
397
|
},
|
|
398
|
+
{
|
|
399
|
+
id: "image-to-pdf",
|
|
400
|
+
category: "convert",
|
|
401
|
+
name: "Image to PDF",
|
|
402
|
+
icon: "image-up",
|
|
403
|
+
note: "Turn images into one PDF.",
|
|
404
|
+
about: "Pick a page size, drag the images into order, and get one PDF.",
|
|
405
|
+
tutorial: {
|
|
406
|
+
intro: "Turn one image or a set into a single PDF.",
|
|
407
|
+
steps: [
|
|
408
|
+
"Drop the images and drag them into the order you want.",
|
|
409
|
+
"Pick A4 or Letter, a margin, and whether each image fits inside the page or fills it.",
|
|
410
|
+
"Press Build PDF. The file downloads from this tab.",
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
keywords: "image to pdf jpg to pdf png to pdf photo to pdf combine images document",
|
|
414
|
+
load: () => import("./tools/image-to-pdf.js"),
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "pdf-to-image",
|
|
418
|
+
category: "convert",
|
|
419
|
+
name: "PDF to Image",
|
|
420
|
+
icon: "file-text",
|
|
421
|
+
note: "Render every page as a PNG or JPEG.",
|
|
422
|
+
about: "Choose a page range and a scale, then take the pages as a ZIP of images.",
|
|
423
|
+
tutorial: {
|
|
424
|
+
intro: "Every page of a PDF as its own image.",
|
|
425
|
+
steps: [
|
|
426
|
+
"Drop a PDF. A preview of the first pages appears.",
|
|
427
|
+
"Set the range, the format, and the scale. 2x is a sharp screen size.",
|
|
428
|
+
"Press Download ZIP for one image per page.",
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
keywords: "pdf to image pdf to png pdf to jpg render page screenshot",
|
|
432
|
+
load: () => import("./tools/pdf-to-image.js"),
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "pdf-merge",
|
|
436
|
+
category: "convert",
|
|
437
|
+
name: "PDF Merge",
|
|
438
|
+
icon: "combine",
|
|
439
|
+
note: "Join several PDFs into one.",
|
|
440
|
+
about: "Files merge from top to bottom, and the pages stay as they were.",
|
|
441
|
+
tutorial: {
|
|
442
|
+
intro: "Combine PDFs without re-rendering them.",
|
|
443
|
+
steps: [
|
|
444
|
+
"Drop two or more PDFs.",
|
|
445
|
+
"Drag or use the arrows to set the order.",
|
|
446
|
+
"Press Merge PDFs. Text stays selectable because the pages are copied, not redrawn.",
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
keywords: "merge pdf combine pdf join pdf append document",
|
|
450
|
+
load: () => import("./tools/pdf-merge.js"),
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "pdf-split",
|
|
454
|
+
category: "convert",
|
|
455
|
+
name: "PDF Split",
|
|
456
|
+
icon: "scissors",
|
|
457
|
+
note: "Cut one PDF into parts.",
|
|
458
|
+
about: "Split by range, by a fixed page count, or one file per page.",
|
|
459
|
+
tutorial: {
|
|
460
|
+
intro: "Turn one PDF into several files.",
|
|
461
|
+
steps: [
|
|
462
|
+
"Drop a PDF.",
|
|
463
|
+
"Pick By range, Every N pages, or One per page.",
|
|
464
|
+
"Press Split and download for a ZIP of the parts.",
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
keywords: "split pdf cut pdf separate pages extract range",
|
|
468
|
+
load: () => import("./tools/pdf-split.js"),
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: "pdf-to-text",
|
|
472
|
+
category: "convert",
|
|
473
|
+
name: "PDF to Text",
|
|
474
|
+
icon: "text-search",
|
|
475
|
+
note: "Copy the text out of a PDF.",
|
|
476
|
+
about: "Each page is labelled, so the order survives the copy.",
|
|
477
|
+
tutorial: {
|
|
478
|
+
intro: "Read the text layer of a PDF.",
|
|
479
|
+
steps: [
|
|
480
|
+
"Drop a PDF.",
|
|
481
|
+
"Set a page range, or leave it empty for every page.",
|
|
482
|
+
"Press Extract text, then copy it or download a .txt. A scan has no text layer, and the tool says so.",
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
keywords: "pdf to text extract text copy pdf txt document",
|
|
486
|
+
load: () => import("./tools/pdf-to-text.js"),
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: "pdf-organizer",
|
|
490
|
+
category: "convert",
|
|
491
|
+
name: "PDF Page Organizer",
|
|
492
|
+
icon: "layout-grid",
|
|
493
|
+
note: "Reorder, rotate and delete pages.",
|
|
494
|
+
about: "Every page is a thumbnail. Drag it, turn it, or drop it.",
|
|
495
|
+
tutorial: {
|
|
496
|
+
intro: "Rearrange a PDF page by page.",
|
|
497
|
+
steps: [
|
|
498
|
+
"Drop a PDF and wait for the thumbnails.",
|
|
499
|
+
"Drag a page to move it, use the arrows, rotate it, or delete it.",
|
|
500
|
+
"Press Save PDF. Your original file is never touched.",
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
keywords: "reorder pdf rotate pdf delete page organize pages arrange",
|
|
504
|
+
load: () => import("./tools/pdf-organizer.js"),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "text-to-pdf",
|
|
508
|
+
category: "convert",
|
|
509
|
+
name: "Text to PDF",
|
|
510
|
+
icon: "file-text",
|
|
511
|
+
note: "Turn plain text into a clean PDF.",
|
|
512
|
+
about: "Wrapped lines, real page breaks, selectable text.",
|
|
513
|
+
tutorial: {
|
|
514
|
+
intro: "Paste text and get a PDF.",
|
|
515
|
+
steps: [
|
|
516
|
+
"Paste the text and add an optional title.",
|
|
517
|
+
"Pick a page size, margin, font size and line height.",
|
|
518
|
+
"Press Build PDF. Long lines wrap and long text breaks onto new pages.",
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
keywords: "text to pdf txt to pdf write document page break",
|
|
522
|
+
load: () => import("./tools/text-to-pdf.js"),
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: "svg-to-png",
|
|
526
|
+
category: "convert",
|
|
527
|
+
name: "SVG to PNG",
|
|
528
|
+
icon: "image-up",
|
|
529
|
+
note: "Render an SVG at any pixel size.",
|
|
530
|
+
about: "Drawn at the size you ask for, so the PNG stays sharp.",
|
|
531
|
+
tutorial: {
|
|
532
|
+
intro: "Turn a vector into a PNG.",
|
|
533
|
+
steps: [
|
|
534
|
+
"Drop an SVG.",
|
|
535
|
+
"Set the width and height, or pick a preset.",
|
|
536
|
+
"Choose a transparent or solid background, then download the PNG.",
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
keywords: "svg to png vector rasterize icon export",
|
|
540
|
+
load: () => import("./tools/svg-to-png.js"),
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: "audio-converter",
|
|
544
|
+
category: "convert",
|
|
545
|
+
name: "Audio Converter",
|
|
546
|
+
icon: "activity",
|
|
547
|
+
note: "Convert audio to WAV.",
|
|
548
|
+
about: "Resample, trim and normalize, then write 16 bit PCM.",
|
|
549
|
+
tutorial: {
|
|
550
|
+
intro: "Decode an audio file and write a clean WAV.",
|
|
551
|
+
steps: [
|
|
552
|
+
"Drop an audio file the browser can decode.",
|
|
553
|
+
"Pick a sample rate and mono or stereo, then trim if you need to.",
|
|
554
|
+
"Press Convert to WAV.",
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
keywords: "audio converter wav mp3 ogg flac resample normalize trim",
|
|
558
|
+
load: () => import("./tools/audio-converter.js"),
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: "video-converter",
|
|
562
|
+
category: "convert",
|
|
563
|
+
name: "Video Converter",
|
|
564
|
+
icon: "video",
|
|
565
|
+
note: "Convert a video to WebM.",
|
|
566
|
+
about: "Or pull the audio track out as Opus.",
|
|
567
|
+
tutorial: {
|
|
568
|
+
intro: "Record a video into WebM, or take its audio.",
|
|
569
|
+
steps: [
|
|
570
|
+
"Drop a video the browser can play.",
|
|
571
|
+
"Pick WebM video or audio only, a resolution and a bitrate.",
|
|
572
|
+
"Press Convert and leave the tab in front. Recording runs at real time.",
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
keywords: "video converter webm mp4 opus audio track extract transcode",
|
|
576
|
+
load: () => import("./tools/video-converter.js"),
|
|
577
|
+
},
|
|
398
578
|
|
|
399
579
|
{
|
|
400
580
|
id: "lorem-ipsum",
|