command line pdf tools [unmaintained]
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add PDF manipulation tools including merge, split, extract, delete, rotate, and organize

+1490 -5
+859
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "adler2" 7 + version = "2.0.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 + 11 + [[package]] 12 + name = "aes" 13 + version = "0.8.4" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 16 + dependencies = [ 17 + "cfg-if", 18 + "cipher", 19 + "cpufeatures", 20 + ] 21 + 22 + [[package]] 23 + name = "android_system_properties" 24 + version = "0.1.5" 25 + source = "registry+https://github.com/rust-lang/crates.io-index" 26 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 27 + dependencies = [ 28 + "libc", 29 + ] 30 + 31 + [[package]] 6 32 name = "anstream" 7 33 version = "0.6.21" 8 34 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 53 79 ] 54 80 55 81 [[package]] 82 + name = "autocfg" 83 + version = "1.5.0" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 86 + 87 + [[package]] 56 88 name = "bitflags" 57 89 version = "2.10.0" 58 90 source = "registry+https://github.com/rust-lang/crates.io-index" 59 91 checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 92 + 93 + [[package]] 94 + name = "block-buffer" 95 + version = "0.10.4" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 98 + dependencies = [ 99 + "generic-array", 100 + ] 101 + 102 + [[package]] 103 + name = "block-padding" 104 + version = "0.3.3" 105 + source = "registry+https://github.com/rust-lang/crates.io-index" 106 + checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" 107 + dependencies = [ 108 + "generic-array", 109 + ] 110 + 111 + [[package]] 112 + name = "bumpalo" 113 + version = "3.19.0" 114 + source = "registry+https://github.com/rust-lang/crates.io-index" 115 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 116 + 117 + [[package]] 118 + name = "bytecount" 119 + version = "0.6.9" 120 + source = "registry+https://github.com/rust-lang/crates.io-index" 121 + checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" 122 + 123 + [[package]] 124 + name = "cbc" 125 + version = "0.1.2" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" 128 + dependencies = [ 129 + "cipher", 130 + ] 131 + 132 + [[package]] 133 + name = "cc" 134 + version = "1.2.49" 135 + source = "registry+https://github.com/rust-lang/crates.io-index" 136 + checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" 137 + dependencies = [ 138 + "find-msvc-tools", 139 + "shlex", 140 + ] 141 + 142 + [[package]] 143 + name = "cfg-if" 144 + version = "1.0.4" 145 + source = "registry+https://github.com/rust-lang/crates.io-index" 146 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 147 + 148 + [[package]] 149 + name = "chrono" 150 + version = "0.4.42" 151 + source = "registry+https://github.com/rust-lang/crates.io-index" 152 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 153 + dependencies = [ 154 + "iana-time-zone", 155 + "num-traits", 156 + "windows-link", 157 + ] 158 + 159 + [[package]] 160 + name = "cipher" 161 + version = "0.4.4" 162 + source = "registry+https://github.com/rust-lang/crates.io-index" 163 + checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 164 + dependencies = [ 165 + "crypto-common", 166 + "inout", 167 + ] 60 168 61 169 [[package]] 62 170 name = "clap" ··· 105 213 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 106 214 107 215 [[package]] 216 + name = "core-foundation-sys" 217 + version = "0.8.7" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 220 + 221 + [[package]] 222 + name = "cpufeatures" 223 + version = "0.2.17" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 226 + dependencies = [ 227 + "libc", 228 + ] 229 + 230 + [[package]] 231 + name = "crc32fast" 232 + version = "1.5.0" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 235 + dependencies = [ 236 + "cfg-if", 237 + ] 238 + 239 + [[package]] 240 + name = "crossbeam-deque" 241 + version = "0.8.6" 242 + source = "registry+https://github.com/rust-lang/crates.io-index" 243 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 244 + dependencies = [ 245 + "crossbeam-epoch", 246 + "crossbeam-utils", 247 + ] 248 + 249 + [[package]] 250 + name = "crossbeam-epoch" 251 + version = "0.9.18" 252 + source = "registry+https://github.com/rust-lang/crates.io-index" 253 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 254 + dependencies = [ 255 + "crossbeam-utils", 256 + ] 257 + 258 + [[package]] 259 + name = "crossbeam-utils" 260 + version = "0.8.21" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 263 + 264 + [[package]] 265 + name = "crypto-common" 266 + version = "0.1.7" 267 + source = "registry+https://github.com/rust-lang/crates.io-index" 268 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 269 + dependencies = [ 270 + "generic-array", 271 + "typenum", 272 + ] 273 + 274 + [[package]] 275 + name = "deranged" 276 + version = "0.5.5" 277 + source = "registry+https://github.com/rust-lang/crates.io-index" 278 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 279 + dependencies = [ 280 + "powerfmt", 281 + ] 282 + 283 + [[package]] 284 + name = "digest" 285 + version = "0.10.7" 286 + source = "registry+https://github.com/rust-lang/crates.io-index" 287 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 288 + dependencies = [ 289 + "block-buffer", 290 + "crypto-common", 291 + ] 292 + 293 + [[package]] 294 + name = "ecb" 295 + version = "0.1.2" 296 + source = "registry+https://github.com/rust-lang/crates.io-index" 297 + checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" 298 + dependencies = [ 299 + "cipher", 300 + ] 301 + 302 + [[package]] 303 + name = "either" 304 + version = "1.15.0" 305 + source = "registry+https://github.com/rust-lang/crates.io-index" 306 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 307 + 308 + [[package]] 309 + name = "encoding_rs" 310 + version = "0.8.35" 311 + source = "registry+https://github.com/rust-lang/crates.io-index" 312 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 313 + dependencies = [ 314 + "cfg-if", 315 + ] 316 + 317 + [[package]] 318 + name = "equivalent" 319 + version = "1.0.2" 320 + source = "registry+https://github.com/rust-lang/crates.io-index" 321 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 322 + 323 + [[package]] 324 + name = "find-msvc-tools" 325 + version = "0.1.5" 326 + source = "registry+https://github.com/rust-lang/crates.io-index" 327 + checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 328 + 329 + [[package]] 330 + name = "flate2" 331 + version = "1.1.5" 332 + source = "registry+https://github.com/rust-lang/crates.io-index" 333 + checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 334 + dependencies = [ 335 + "crc32fast", 336 + "miniz_oxide", 337 + ] 338 + 339 + [[package]] 340 + name = "generic-array" 341 + version = "0.14.7" 342 + source = "registry+https://github.com/rust-lang/crates.io-index" 343 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 344 + dependencies = [ 345 + "typenum", 346 + "version_check", 347 + ] 348 + 349 + [[package]] 108 350 name = "getopts" 109 351 version = "0.2.24" 110 352 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 114 356 ] 115 357 116 358 [[package]] 359 + name = "getrandom" 360 + version = "0.3.4" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 363 + dependencies = [ 364 + "cfg-if", 365 + "libc", 366 + "r-efi", 367 + "wasip2", 368 + ] 369 + 370 + [[package]] 371 + name = "hashbrown" 372 + version = "0.16.1" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 375 + 376 + [[package]] 117 377 name = "heck" 118 378 version = "0.5.0" 119 379 source = "registry+https://github.com/rust-lang/crates.io-index" 120 380 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 121 381 122 382 [[package]] 383 + name = "iana-time-zone" 384 + version = "0.1.64" 385 + source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 387 + dependencies = [ 388 + "android_system_properties", 389 + "core-foundation-sys", 390 + "iana-time-zone-haiku", 391 + "js-sys", 392 + "log", 393 + "wasm-bindgen", 394 + "windows-core", 395 + ] 396 + 397 + [[package]] 398 + name = "iana-time-zone-haiku" 399 + version = "0.1.2" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 402 + dependencies = [ 403 + "cc", 404 + ] 405 + 406 + [[package]] 407 + name = "indexmap" 408 + version = "2.12.1" 409 + source = "registry+https://github.com/rust-lang/crates.io-index" 410 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 411 + dependencies = [ 412 + "equivalent", 413 + "hashbrown", 414 + ] 415 + 416 + [[package]] 417 + name = "inout" 418 + version = "0.1.4" 419 + source = "registry+https://github.com/rust-lang/crates.io-index" 420 + checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 421 + dependencies = [ 422 + "block-padding", 423 + "generic-array", 424 + ] 425 + 426 + [[package]] 123 427 name = "is_terminal_polyfill" 124 428 version = "1.70.2" 125 429 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 132 436 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 133 437 134 438 [[package]] 439 + name = "jiff" 440 + version = "0.2.16" 441 + source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" 443 + dependencies = [ 444 + "jiff-static", 445 + "jiff-tzdb-platform", 446 + "log", 447 + "portable-atomic", 448 + "portable-atomic-util", 449 + "serde_core", 450 + "windows-sys", 451 + ] 452 + 453 + [[package]] 454 + name = "jiff-static" 455 + version = "0.2.16" 456 + source = "registry+https://github.com/rust-lang/crates.io-index" 457 + checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" 458 + dependencies = [ 459 + "proc-macro2", 460 + "quote", 461 + "syn", 462 + ] 463 + 464 + [[package]] 465 + name = "jiff-tzdb" 466 + version = "0.1.4" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" 469 + 470 + [[package]] 471 + name = "jiff-tzdb-platform" 472 + version = "0.1.3" 473 + source = "registry+https://github.com/rust-lang/crates.io-index" 474 + checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 475 + dependencies = [ 476 + "jiff-tzdb", 477 + ] 478 + 479 + [[package]] 480 + name = "js-sys" 481 + version = "0.3.83" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 484 + dependencies = [ 485 + "once_cell", 486 + "wasm-bindgen", 487 + ] 488 + 489 + [[package]] 490 + name = "libc" 491 + version = "0.2.178" 492 + source = "registry+https://github.com/rust-lang/crates.io-index" 493 + checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 494 + 495 + [[package]] 496 + name = "log" 497 + version = "0.4.29" 498 + source = "registry+https://github.com/rust-lang/crates.io-index" 499 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 500 + 501 + [[package]] 502 + name = "lopdf" 503 + version = "0.38.0" 504 + source = "registry+https://github.com/rust-lang/crates.io-index" 505 + checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" 506 + dependencies = [ 507 + "aes", 508 + "bitflags", 509 + "cbc", 510 + "chrono", 511 + "ecb", 512 + "encoding_rs", 513 + "flate2", 514 + "getrandom", 515 + "indexmap", 516 + "itoa", 517 + "jiff", 518 + "log", 519 + "md-5", 520 + "nom", 521 + "nom_locate", 522 + "rand", 523 + "rangemap", 524 + "rayon", 525 + "sha2", 526 + "stringprep", 527 + "thiserror", 528 + "time", 529 + "ttf-parser", 530 + "weezl", 531 + ] 532 + 533 + [[package]] 534 + name = "md-5" 535 + version = "0.10.6" 536 + source = "registry+https://github.com/rust-lang/crates.io-index" 537 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 538 + dependencies = [ 539 + "cfg-if", 540 + "digest", 541 + ] 542 + 543 + [[package]] 135 544 name = "memchr" 136 545 version = "2.7.6" 137 546 source = "registry+https://github.com/rust-lang/crates.io-index" 138 547 checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 139 548 140 549 [[package]] 550 + name = "miniz_oxide" 551 + version = "0.8.9" 552 + source = "registry+https://github.com/rust-lang/crates.io-index" 553 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 554 + dependencies = [ 555 + "adler2", 556 + "simd-adler32", 557 + ] 558 + 559 + [[package]] 560 + name = "nom" 561 + version = "8.0.0" 562 + source = "registry+https://github.com/rust-lang/crates.io-index" 563 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 564 + dependencies = [ 565 + "memchr", 566 + ] 567 + 568 + [[package]] 569 + name = "nom_locate" 570 + version = "5.0.0" 571 + source = "registry+https://github.com/rust-lang/crates.io-index" 572 + checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" 573 + dependencies = [ 574 + "bytecount", 575 + "memchr", 576 + "nom", 577 + ] 578 + 579 + [[package]] 580 + name = "num-conv" 581 + version = "0.1.0" 582 + source = "registry+https://github.com/rust-lang/crates.io-index" 583 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 584 + 585 + [[package]] 586 + name = "num-traits" 587 + version = "0.2.19" 588 + source = "registry+https://github.com/rust-lang/crates.io-index" 589 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 590 + dependencies = [ 591 + "autocfg", 592 + ] 593 + 594 + [[package]] 595 + name = "once_cell" 596 + version = "1.21.3" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 599 + 600 + [[package]] 141 601 name = "once_cell_polyfill" 142 602 version = "1.70.2" 143 603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 174 634 name = "picopdf-core" 175 635 version = "0.1.0" 176 636 dependencies = [ 637 + "lopdf", 177 638 "pdf-writer", 178 639 "pulldown-cmark", 179 640 "ttf-parser", 180 641 ] 181 642 182 643 [[package]] 644 + name = "portable-atomic" 645 + version = "1.11.1" 646 + source = "registry+https://github.com/rust-lang/crates.io-index" 647 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 648 + 649 + [[package]] 650 + name = "portable-atomic-util" 651 + version = "0.2.4" 652 + source = "registry+https://github.com/rust-lang/crates.io-index" 653 + checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 654 + dependencies = [ 655 + "portable-atomic", 656 + ] 657 + 658 + [[package]] 659 + name = "powerfmt" 660 + version = "0.2.0" 661 + source = "registry+https://github.com/rust-lang/crates.io-index" 662 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 663 + 664 + [[package]] 665 + name = "ppv-lite86" 666 + version = "0.2.21" 667 + source = "registry+https://github.com/rust-lang/crates.io-index" 668 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 669 + dependencies = [ 670 + "zerocopy", 671 + ] 672 + 673 + [[package]] 183 674 name = "proc-macro2" 184 675 version = "1.0.103" 185 676 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 217 708 ] 218 709 219 710 [[package]] 711 + name = "r-efi" 712 + version = "5.3.0" 713 + source = "registry+https://github.com/rust-lang/crates.io-index" 714 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 715 + 716 + [[package]] 717 + name = "rand" 718 + version = "0.9.2" 719 + source = "registry+https://github.com/rust-lang/crates.io-index" 720 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 721 + dependencies = [ 722 + "rand_chacha", 723 + "rand_core", 724 + ] 725 + 726 + [[package]] 727 + name = "rand_chacha" 728 + version = "0.9.0" 729 + source = "registry+https://github.com/rust-lang/crates.io-index" 730 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 731 + dependencies = [ 732 + "ppv-lite86", 733 + "rand_core", 734 + ] 735 + 736 + [[package]] 737 + name = "rand_core" 738 + version = "0.9.3" 739 + source = "registry+https://github.com/rust-lang/crates.io-index" 740 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 741 + dependencies = [ 742 + "getrandom", 743 + ] 744 + 745 + [[package]] 746 + name = "rangemap" 747 + version = "1.7.0" 748 + source = "registry+https://github.com/rust-lang/crates.io-index" 749 + checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c" 750 + 751 + [[package]] 752 + name = "rayon" 753 + version = "1.11.0" 754 + source = "registry+https://github.com/rust-lang/crates.io-index" 755 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 756 + dependencies = [ 757 + "either", 758 + "rayon-core", 759 + ] 760 + 761 + [[package]] 762 + name = "rayon-core" 763 + version = "1.13.0" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 766 + dependencies = [ 767 + "crossbeam-deque", 768 + "crossbeam-utils", 769 + ] 770 + 771 + [[package]] 772 + name = "rustversion" 773 + version = "1.0.22" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 776 + 777 + [[package]] 220 778 name = "ryu" 221 779 version = "1.0.20" 222 780 source = "registry+https://github.com/rust-lang/crates.io-index" 223 781 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 224 782 225 783 [[package]] 784 + name = "serde" 785 + version = "1.0.228" 786 + source = "registry+https://github.com/rust-lang/crates.io-index" 787 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 788 + dependencies = [ 789 + "serde_core", 790 + ] 791 + 792 + [[package]] 793 + name = "serde_core" 794 + version = "1.0.228" 795 + source = "registry+https://github.com/rust-lang/crates.io-index" 796 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 797 + dependencies = [ 798 + "serde_derive", 799 + ] 800 + 801 + [[package]] 802 + name = "serde_derive" 803 + version = "1.0.228" 804 + source = "registry+https://github.com/rust-lang/crates.io-index" 805 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 806 + dependencies = [ 807 + "proc-macro2", 808 + "quote", 809 + "syn", 810 + ] 811 + 812 + [[package]] 813 + name = "sha2" 814 + version = "0.10.9" 815 + source = "registry+https://github.com/rust-lang/crates.io-index" 816 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 817 + dependencies = [ 818 + "cfg-if", 819 + "cpufeatures", 820 + "digest", 821 + ] 822 + 823 + [[package]] 824 + name = "shlex" 825 + version = "1.3.0" 826 + source = "registry+https://github.com/rust-lang/crates.io-index" 827 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 828 + 829 + [[package]] 830 + name = "simd-adler32" 831 + version = "0.3.8" 832 + source = "registry+https://github.com/rust-lang/crates.io-index" 833 + checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 834 + 835 + [[package]] 836 + name = "stringprep" 837 + version = "0.1.5" 838 + source = "registry+https://github.com/rust-lang/crates.io-index" 839 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 840 + dependencies = [ 841 + "unicode-bidi", 842 + "unicode-normalization", 843 + "unicode-properties", 844 + ] 845 + 846 + [[package]] 226 847 name = "strsim" 227 848 version = "0.11.1" 228 849 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 240 861 ] 241 862 242 863 [[package]] 864 + name = "thiserror" 865 + version = "2.0.17" 866 + source = "registry+https://github.com/rust-lang/crates.io-index" 867 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 868 + dependencies = [ 869 + "thiserror-impl", 870 + ] 871 + 872 + [[package]] 873 + name = "thiserror-impl" 874 + version = "2.0.17" 875 + source = "registry+https://github.com/rust-lang/crates.io-index" 876 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 877 + dependencies = [ 878 + "proc-macro2", 879 + "quote", 880 + "syn", 881 + ] 882 + 883 + [[package]] 884 + name = "time" 885 + version = "0.3.44" 886 + source = "registry+https://github.com/rust-lang/crates.io-index" 887 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 888 + dependencies = [ 889 + "deranged", 890 + "itoa", 891 + "num-conv", 892 + "powerfmt", 893 + "serde", 894 + "time-core", 895 + "time-macros", 896 + ] 897 + 898 + [[package]] 899 + name = "time-core" 900 + version = "0.1.6" 901 + source = "registry+https://github.com/rust-lang/crates.io-index" 902 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 903 + 904 + [[package]] 905 + name = "time-macros" 906 + version = "0.2.24" 907 + source = "registry+https://github.com/rust-lang/crates.io-index" 908 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 909 + dependencies = [ 910 + "num-conv", 911 + "time-core", 912 + ] 913 + 914 + [[package]] 915 + name = "tinyvec" 916 + version = "1.10.0" 917 + source = "registry+https://github.com/rust-lang/crates.io-index" 918 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 919 + dependencies = [ 920 + "tinyvec_macros", 921 + ] 922 + 923 + [[package]] 924 + name = "tinyvec_macros" 925 + version = "0.1.1" 926 + source = "registry+https://github.com/rust-lang/crates.io-index" 927 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 928 + 929 + [[package]] 243 930 name = "ttf-parser" 244 931 version = "0.25.1" 245 932 source = "registry+https://github.com/rust-lang/crates.io-index" 246 933 checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 934 + 935 + [[package]] 936 + name = "typenum" 937 + version = "1.19.0" 938 + source = "registry+https://github.com/rust-lang/crates.io-index" 939 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 247 940 248 941 [[package]] 249 942 name = "unicase" 250 943 version = "2.8.1" 251 944 source = "registry+https://github.com/rust-lang/crates.io-index" 252 945 checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 946 + 947 + [[package]] 948 + name = "unicode-bidi" 949 + version = "0.3.18" 950 + source = "registry+https://github.com/rust-lang/crates.io-index" 951 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 253 952 254 953 [[package]] 255 954 name = "unicode-ident" ··· 258 957 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 259 958 260 959 [[package]] 960 + name = "unicode-normalization" 961 + version = "0.1.25" 962 + source = "registry+https://github.com/rust-lang/crates.io-index" 963 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 964 + dependencies = [ 965 + "tinyvec", 966 + ] 967 + 968 + [[package]] 969 + name = "unicode-properties" 970 + version = "0.1.4" 971 + source = "registry+https://github.com/rust-lang/crates.io-index" 972 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 973 + 974 + [[package]] 261 975 name = "unicode-width" 262 976 version = "0.2.2" 263 977 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 270 984 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 271 985 272 986 [[package]] 987 + name = "version_check" 988 + version = "0.9.5" 989 + source = "registry+https://github.com/rust-lang/crates.io-index" 990 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 991 + 992 + [[package]] 993 + name = "wasip2" 994 + version = "1.0.1+wasi-0.2.4" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 997 + dependencies = [ 998 + "wit-bindgen", 999 + ] 1000 + 1001 + [[package]] 1002 + name = "wasm-bindgen" 1003 + version = "0.2.106" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 1006 + dependencies = [ 1007 + "cfg-if", 1008 + "once_cell", 1009 + "rustversion", 1010 + "wasm-bindgen-macro", 1011 + "wasm-bindgen-shared", 1012 + ] 1013 + 1014 + [[package]] 1015 + name = "wasm-bindgen-macro" 1016 + version = "0.2.106" 1017 + source = "registry+https://github.com/rust-lang/crates.io-index" 1018 + checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 1019 + dependencies = [ 1020 + "quote", 1021 + "wasm-bindgen-macro-support", 1022 + ] 1023 + 1024 + [[package]] 1025 + name = "wasm-bindgen-macro-support" 1026 + version = "0.2.106" 1027 + source = "registry+https://github.com/rust-lang/crates.io-index" 1028 + checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 1029 + dependencies = [ 1030 + "bumpalo", 1031 + "proc-macro2", 1032 + "quote", 1033 + "syn", 1034 + "wasm-bindgen-shared", 1035 + ] 1036 + 1037 + [[package]] 1038 + name = "wasm-bindgen-shared" 1039 + version = "0.2.106" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 1042 + dependencies = [ 1043 + "unicode-ident", 1044 + ] 1045 + 1046 + [[package]] 1047 + name = "weezl" 1048 + version = "0.1.12" 1049 + source = "registry+https://github.com/rust-lang/crates.io-index" 1050 + checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" 1051 + 1052 + [[package]] 1053 + name = "windows-core" 1054 + version = "0.62.2" 1055 + source = "registry+https://github.com/rust-lang/crates.io-index" 1056 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 1057 + dependencies = [ 1058 + "windows-implement", 1059 + "windows-interface", 1060 + "windows-link", 1061 + "windows-result", 1062 + "windows-strings", 1063 + ] 1064 + 1065 + [[package]] 1066 + name = "windows-implement" 1067 + version = "0.60.2" 1068 + source = "registry+https://github.com/rust-lang/crates.io-index" 1069 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 1070 + dependencies = [ 1071 + "proc-macro2", 1072 + "quote", 1073 + "syn", 1074 + ] 1075 + 1076 + [[package]] 1077 + name = "windows-interface" 1078 + version = "0.59.3" 1079 + source = "registry+https://github.com/rust-lang/crates.io-index" 1080 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 1081 + dependencies = [ 1082 + "proc-macro2", 1083 + "quote", 1084 + "syn", 1085 + ] 1086 + 1087 + [[package]] 273 1088 name = "windows-link" 274 1089 version = "0.2.1" 275 1090 source = "registry+https://github.com/rust-lang/crates.io-index" 276 1091 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1092 + 1093 + [[package]] 1094 + name = "windows-result" 1095 + version = "0.4.1" 1096 + source = "registry+https://github.com/rust-lang/crates.io-index" 1097 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 1098 + dependencies = [ 1099 + "windows-link", 1100 + ] 1101 + 1102 + [[package]] 1103 + name = "windows-strings" 1104 + version = "0.5.1" 1105 + source = "registry+https://github.com/rust-lang/crates.io-index" 1106 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 1107 + dependencies = [ 1108 + "windows-link", 1109 + ] 277 1110 278 1111 [[package]] 279 1112 name = "windows-sys" ··· 283 1116 dependencies = [ 284 1117 "windows-link", 285 1118 ] 1119 + 1120 + [[package]] 1121 + name = "wit-bindgen" 1122 + version = "0.46.0" 1123 + source = "registry+https://github.com/rust-lang/crates.io-index" 1124 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1125 + 1126 + [[package]] 1127 + name = "zerocopy" 1128 + version = "0.8.31" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 1131 + dependencies = [ 1132 + "zerocopy-derive", 1133 + ] 1134 + 1135 + [[package]] 1136 + name = "zerocopy-derive" 1137 + version = "0.8.31" 1138 + source = "registry+https://github.com/rust-lang/crates.io-index" 1139 + checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 1140 + dependencies = [ 1141 + "proc-macro2", 1142 + "quote", 1143 + "syn", 1144 + ]
+29
TODO.md
··· 1 + # To-Do 2 + 3 + ## Tools 4 + 5 + - [ ] Merge: Combine multiple PDFs into one. 6 + - [ ] Split: Extract or split pages into separate PDFs. 7 + - [ ] Rotate: Rotate one or all pages. 8 + - [ ] Delete Pages: Remove pages. 9 + - [ ] Extract Pages: Pull out selected pages into a new file. 10 + - [ ] Organize: Rearrange, add, delete, and rotate pages in one UI. 11 + - [ ] Compress: Reduce file size with basic/strong compression. 12 + 13 + ## From 14 + 15 + - [ ] PDF to Word - Convert PDFs to editable Word docs. 16 + - [ ] PDF to Excel - Convert to editable spreadsheets. 17 + - [ ] PDF to PPT - Convert to PowerPoint. 18 + - [ ] PDF to JPG - Export pages or images from PDF as images. 19 + 20 + ## To 21 + 22 + - [ ] Word to PDF 23 + - [ ] Excel to PDF 24 + - [ ] PPT to PDF 25 + - [ ] JPG to PDF (also handles PNG, BMP, GIF, TIFF). 26 + 27 + ## OCR 28 + 29 + - [ ] Make scanned PDFs searchable (optical character recognition).
+233 -5
crates/cli/src/main.rs
··· 35 35 #[arg(long)] 36 36 font_mono: Option<PathBuf>, 37 37 }, 38 + 39 + /// PDF manipulation tools 40 + #[command(subcommand)] 41 + Tool(ToolCommands), 42 + } 43 + 44 + #[derive(Subcommand)] 45 + enum ToolCommands { 46 + /// Merge multiple PDF files into one 47 + Merge { 48 + /// Input PDF files to merge 49 + #[arg(required = true)] 50 + inputs: Vec<PathBuf>, 51 + 52 + /// Output PDF file 53 + #[arg(short, long)] 54 + output: PathBuf, 55 + }, 56 + 57 + /// Split PDF into separate files 58 + Split { 59 + /// Input PDF file 60 + #[arg(short, long)] 61 + input: PathBuf, 62 + 63 + /// Output directory for split PDFs 64 + #[arg(short, long)] 65 + output_dir: PathBuf, 66 + 67 + /// Optional page ranges (e.g., "1-3,5,7-9") 68 + #[arg(short, long)] 69 + ranges: Option<String>, 70 + }, 71 + 72 + /// Extract specific pages 73 + Extract { 74 + /// Input PDF file 75 + #[arg(short, long)] 76 + input: PathBuf, 77 + 78 + /// Output PDF file 79 + #[arg(short, long)] 80 + output: PathBuf, 81 + 82 + /// Pages to extract (e.g., "1,3,5-7") 83 + #[arg(short, long)] 84 + pages: String, 85 + }, 86 + 87 + /// Delete specific pages 88 + Delete { 89 + /// Input PDF file 90 + #[arg(short, long)] 91 + input: PathBuf, 92 + 93 + /// Output PDF file 94 + #[arg(short, long)] 95 + output: PathBuf, 96 + 97 + /// Pages to delete (e.g., "2,4,6-8") 98 + #[arg(short, long)] 99 + pages: String, 100 + }, 101 + 102 + /// Rotate pages 103 + Rotate { 104 + /// Input PDF file 105 + #[arg(short, long)] 106 + input: PathBuf, 107 + 108 + /// Output PDF file 109 + #[arg(short, long)] 110 + output: PathBuf, 111 + 112 + /// Rotation angle (90, 180, or 270 degrees) 113 + #[arg(short = 'a', long, default_value = "90")] 114 + angle: i32, 115 + 116 + /// Pages to rotate (empty = all pages, e.g., "1,3,5-7") 117 + #[arg(short, long)] 118 + pages: Option<String>, 119 + }, 120 + 121 + /// Reorganize pages in specified order 122 + Organize { 123 + /// Input PDF file 124 + #[arg(short, long)] 125 + input: PathBuf, 126 + 127 + /// Output PDF file 128 + #[arg(short, long)] 129 + output: PathBuf, 130 + 131 + /// New page order (e.g., "3,1,2,4-6") 132 + #[arg(short = 'n', long)] 133 + new_order: String, 134 + }, 38 135 } 39 136 40 137 fn main() { 41 138 let cli = Cli::parse(); 42 139 43 - match cli.command { 140 + let result = match cli.command { 44 141 Commands::Write { input, output, font_regular, font_bold, font_mono } => { 45 - if let Err(e) = write_command(&input, &output, &font_regular, &font_bold, &font_mono) { 46 - eprintln!("{} {}", "Error:".red().bold(), e); 47 - std::process::exit(1); 48 - } 142 + write_command(&input, &output, &font_regular, &font_bold, &font_mono) 49 143 } 144 + Commands::Tool(tool_cmd) => handle_tool_command(tool_cmd), 145 + }; 146 + 147 + if let Err(e) = result { 148 + eprintln!("{} {}", "Error:".red().bold(), e); 149 + std::process::exit(1); 50 150 } 51 151 } 52 152 ··· 97 197 98 198 Ok(()) 99 199 } 200 + 201 + fn handle_tool_command(cmd: ToolCommands) -> Result<(), Box<dyn std::error::Error>> { 202 + match cmd { 203 + ToolCommands::Merge { inputs, output } => { 204 + println!("{} {} PDF files...", "Merging".cyan().bold(), inputs.len()); 205 + let input_paths: Vec<&str> = inputs.iter().map(|p| p.to_str().unwrap()).collect(); 206 + let pdf_bytes = picopdf_core::tools::merge_pdfs(&input_paths)?; 207 + fs::write(&output, pdf_bytes)?; 208 + println!("{} Merged to {}", "✓".green().bold(), output.display()); 209 + } 210 + 211 + ToolCommands::Split { input, output_dir, ranges } => { 212 + fs::create_dir_all(&output_dir)?; 213 + 214 + match ranges { 215 + Some(ranges_str) => { 216 + println!("{} PDF by ranges...", "Splitting".cyan().bold()); 217 + let ranges = parse_page_ranges(&ranges_str)?; 218 + let pdfs = picopdf_core::tools::split_pdf_by_ranges(input.to_str().unwrap(), &ranges)?; 219 + 220 + for (idx, pdf_bytes) in pdfs.iter().enumerate() { 221 + let output_file = output_dir.join(format!("split_{}.pdf", idx + 1)); 222 + fs::write(&output_file, pdf_bytes)?; 223 + println!("{} Created {}", "✓".green().bold(), output_file.display()); 224 + } 225 + } 226 + None => { 227 + println!("{} PDF into individual pages...", "Splitting".cyan().bold()); 228 + let pdfs = picopdf_core::tools::split_pdf(input.to_str().unwrap())?; 229 + 230 + for (idx, pdf_bytes) in pdfs.iter().enumerate() { 231 + let output_file = output_dir.join(format!("page_{}.pdf", idx + 1)); 232 + fs::write(&output_file, pdf_bytes)?; 233 + } 234 + println!( 235 + "{} Split into {} pages in {}", 236 + "✓".green().bold(), 237 + pdfs.len(), 238 + output_dir.display() 239 + ); 240 + } 241 + } 242 + } 243 + 244 + ToolCommands::Extract { input, output, pages } => { 245 + println!("{} pages {}...", "Extracting".cyan().bold(), pages); 246 + let page_numbers = parse_page_list(&pages)?; 247 + let pdf_bytes = picopdf_core::tools::extract_pages(input.to_str().unwrap(), &page_numbers)?; 248 + fs::write(&output, pdf_bytes)?; 249 + println!("{} Extracted to {}", "✓".green().bold(), output.display()); 250 + } 251 + 252 + ToolCommands::Delete { input, output, pages } => { 253 + println!("{} pages {}...", "Deleting".cyan().bold(), pages); 254 + let page_numbers = parse_page_list(&pages)?; 255 + let pdf_bytes = picopdf_core::tools::delete_pages(input.to_str().unwrap(), &page_numbers)?; 256 + fs::write(&output, pdf_bytes)?; 257 + println!("{} Deleted pages, saved to {}", "✓".green().bold(), output.display()); 258 + } 259 + 260 + ToolCommands::Rotate { input, output, angle, pages } => { 261 + let rotation = picopdf_core::tools::Rotation::from_degrees(angle)?; 262 + let page_numbers = if let Some(pages_str) = pages { parse_page_list(&pages_str)? } else { vec![] }; 263 + 264 + println!("{} pages by {} degrees...", "Rotating".cyan().bold(), angle); 265 + let pdf_bytes = picopdf_core::tools::rotate_pages(input.to_str().unwrap(), &page_numbers, rotation)?; 266 + fs::write(&output, pdf_bytes)?; 267 + println!("{} Rotated, saved to {}", "✓".green().bold(), output.display()); 268 + } 269 + 270 + ToolCommands::Organize { input, output, new_order } => { 271 + println!("{} pages...", "Reorganizing".cyan().bold()); 272 + let order = parse_page_list(&new_order)?; 273 + let pdf_bytes = picopdf_core::tools::organize_pages(input.to_str().unwrap(), &order)?; 274 + fs::write(&output, pdf_bytes)?; 275 + println!("{} Reorganized, saved to {}", "✓".green().bold(), output.display()); 276 + } 277 + } 278 + 279 + Ok(()) 280 + } 281 + 282 + /// Parses a page list string like "1,3,5-7" into a vector of page numbers. 283 + fn parse_page_list(pages_str: &str) -> Result<Vec<u32>, Box<dyn std::error::Error>> { 284 + let mut pages = Vec::new(); 285 + 286 + for part in pages_str.split(',') { 287 + let part = part.trim(); 288 + if part.contains('-') { 289 + let range_parts: Vec<&str> = part.split('-').collect(); 290 + if range_parts.len() != 2 { 291 + return Err(format!("Invalid page range: {}", part).into()); 292 + } 293 + let start: u32 = range_parts[0].parse()?; 294 + let end: u32 = range_parts[1].parse()?; 295 + for page in start..=end { 296 + pages.push(page); 297 + } 298 + } else { 299 + pages.push(part.parse()?); 300 + } 301 + } 302 + 303 + Ok(pages) 304 + } 305 + 306 + /// Parses page ranges like "1-3,5,7-9" into tuples of (start, end). 307 + fn parse_page_ranges(ranges_str: &str) -> Result<Vec<(u32, u32)>, Box<dyn std::error::Error>> { 308 + let mut ranges = Vec::new(); 309 + 310 + for part in ranges_str.split(',') { 311 + let part = part.trim(); 312 + if part.contains('-') { 313 + let range_parts: Vec<&str> = part.split('-').collect(); 314 + if range_parts.len() != 2 { 315 + return Err(format!("Invalid page range: {}", part).into()); 316 + } 317 + let start: u32 = range_parts[0].parse()?; 318 + let end: u32 = range_parts[1].parse()?; 319 + ranges.push((start, end)); 320 + } else { 321 + let page: u32 = part.parse()?; 322 + ranges.push((page, page)); 323 + } 324 + } 325 + 326 + Ok(ranges) 327 + }
+1
crates/core/Cargo.toml
··· 7 7 pdf-writer = "0.14" 8 8 pulldown-cmark = "0.13" 9 9 ttf-parser = "0.25" 10 + lopdf = "0.38" 10 11 11 12 [lints] 12 13 workspace = true
+1
crates/core/src/lib.rs
··· 1 1 pub mod md; 2 + pub mod tools;
+40
crates/core/src/tools/delete.rs
··· 1 + use lopdf::Document; 2 + use std::io; 3 + 4 + /// Deletes specific pages from a PDF document. 5 + /// 6 + /// Page numbers are 1-indexed. Returns a new PDF with the specified pages removed. 7 + pub fn delete_pages(input_file: &str, page_numbers: &[u32]) -> io::Result<Vec<u8>> { 8 + if page_numbers.is_empty() { 9 + return std::fs::read(input_file); 10 + } 11 + 12 + let doc = Document::load(input_file).map_err(|e| { 13 + io::Error::new( 14 + io::ErrorKind::InvalidData, 15 + format!("Failed to load PDF '{}': {}", input_file, e), 16 + ) 17 + })?; 18 + 19 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 20 + let max_page = *all_pages.iter().max().unwrap_or(&0); 21 + 22 + for &page_num in page_numbers { 23 + if page_num < 1 || page_num > max_page { 24 + return Err(io::Error::new( 25 + io::ErrorKind::InvalidInput, 26 + format!("Page {} is out of range (1-{})", page_num, max_page), 27 + )); 28 + } 29 + } 30 + 31 + let mut output_doc = doc; 32 + output_doc.delete_pages(page_numbers); 33 + 34 + let mut output = Vec::new(); 35 + output_doc 36 + .save_to(&mut output) 37 + .map_err(|e| io::Error::other(format!("Failed to save PDF: {}", e)))?; 38 + 39 + Ok(output) 40 + }
+51
crates/core/src/tools/extract.rs
··· 1 + use lopdf::Document; 2 + use std::io; 3 + 4 + /// Extracts specific pages from a PDF into a new PDF document. 5 + /// 6 + /// Page numbers are 1-indexed and can be specified in any order. 7 + /// The output PDF will contain pages in the order specified. 8 + pub fn extract_pages(input_file: &str, page_numbers: &[u32]) -> io::Result<Vec<u8>> { 9 + if page_numbers.is_empty() { 10 + return Err(io::Error::new( 11 + io::ErrorKind::InvalidInput, 12 + "No pages specified for extraction", 13 + )); 14 + } 15 + 16 + let doc = Document::load(input_file).map_err(|e| { 17 + io::Error::new( 18 + io::ErrorKind::InvalidData, 19 + format!("Failed to load PDF '{}': {}", input_file, e), 20 + ) 21 + })?; 22 + 23 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 24 + let max_page = *all_pages.iter().max().unwrap_or(&0); 25 + 26 + for &page_num in page_numbers { 27 + if page_num < 1 || page_num > max_page { 28 + return Err(io::Error::new( 29 + io::ErrorKind::InvalidInput, 30 + format!("Page {} is out of range (1-{})", page_num, max_page), 31 + )); 32 + } 33 + } 34 + 35 + let mut output_doc = doc; 36 + 37 + let pages_to_delete: Vec<u32> = all_pages 38 + .iter() 39 + .filter(|&p| !page_numbers.contains(p)) 40 + .copied() 41 + .collect(); 42 + 43 + output_doc.delete_pages(&pages_to_delete); 44 + 45 + let mut output = Vec::new(); 46 + output_doc 47 + .save_to(&mut output) 48 + .map_err(|e| io::Error::other(format!("Failed to save PDF: {}", e)))?; 49 + 50 + Ok(output) 51 + }
+50
crates/core/src/tools/merge.rs
··· 1 + use lopdf::Document; 2 + use std::io; 3 + 4 + /// Merges multiple PDF files into a single PDF document. 5 + /// 6 + /// Takes a list of PDF file paths and combines all pages from each document in the order provided. Uses a simple concatenation approach. 7 + pub fn merge_pdfs(input_files: &[&str]) -> io::Result<Vec<u8>> { 8 + if input_files.is_empty() { 9 + return Err(io::Error::new(io::ErrorKind::InvalidInput, "No input files provided")); 10 + } 11 + 12 + if input_files.len() == 1 { 13 + return std::fs::read(input_files[0]); 14 + } 15 + 16 + let first_doc = Document::load(input_files[0]).map_err(|e| { 17 + io::Error::new( 18 + io::ErrorKind::InvalidData, 19 + format!("Failed to load PDF '{}': {}", input_files[0], e), 20 + ) 21 + })?; 22 + 23 + let mut output_doc = first_doc; 24 + 25 + for file_path in &input_files[1..] { 26 + let doc = Document::load(file_path).map_err(|e| { 27 + io::Error::new( 28 + io::ErrorKind::InvalidData, 29 + format!("Failed to load PDF '{}': {}", file_path, e), 30 + ) 31 + })?; 32 + 33 + let pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 34 + 35 + for page_num in pages { 36 + if let Some(&page_id) = doc.get_pages().get(&page_num) 37 + && let Ok(page_obj) = doc.get_object(page_id) 38 + { 39 + output_doc.add_object(page_obj.clone()); 40 + } 41 + } 42 + } 43 + 44 + let mut output = Vec::new(); 45 + output_doc 46 + .save_to(&mut output) 47 + .map_err(|e| io::Error::other(format!("Failed to save PDF: {}", e)))?; 48 + 49 + Ok(output) 50 + }
+17
crates/core/src/tools/mod.rs
··· 1 + //! PDF manipulation tools 2 + //! 3 + //! Provides utilities for working with PDF files including merging, splitting, extracting, deleting, rotating, and reorganizing pages. 4 + 5 + pub mod delete; 6 + pub mod extract; 7 + pub mod merge; 8 + pub mod organize; 9 + pub mod rotate; 10 + pub mod split; 11 + 12 + pub use delete::delete_pages; 13 + pub use extract::extract_pages; 14 + pub use merge::merge_pdfs; 15 + pub use organize::organize_pages; 16 + pub use rotate::{Rotation, rotate_pages}; 17 + pub use split::{split_pdf, split_pdf_by_ranges};
+57
crates/core/src/tools/organize.rs
··· 1 + use lopdf::Document; 2 + use std::io; 3 + 4 + /// Reorganizes pages in a PDF according to the specified order. 5 + /// 6 + /// The new_order vector specifies the page numbers (1-indexed) in the desired order. 7 + /// For example, [3, 1, 2] would reorder the pages so page 3 becomes first, followed by page 1, then page 2. 8 + pub fn organize_pages(input_file: &str, new_order: &[u32]) -> io::Result<Vec<u8>> { 9 + let doc = Document::load(input_file).map_err(|e| { 10 + io::Error::new( 11 + io::ErrorKind::InvalidData, 12 + format!("Failed to load PDF '{}': {}", input_file, e), 13 + ) 14 + })?; 15 + 16 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 17 + let max_page = *all_pages.iter().max().unwrap_or(&0); 18 + 19 + if new_order.is_empty() { 20 + return std::fs::read(input_file); 21 + } 22 + 23 + for &page_num in new_order { 24 + if page_num < 1 || page_num > max_page { 25 + return Err(io::Error::new( 26 + io::ErrorKind::InvalidInput, 27 + format!("Page {} is out of range (1-{})", page_num, max_page), 28 + )); 29 + } 30 + } 31 + 32 + let first_page_num = new_order[0]; 33 + let mut output_doc = doc.clone(); 34 + let pages_to_delete: Vec<u32> = all_pages.iter().filter(|&&p| p != first_page_num).copied().collect(); 35 + output_doc.delete_pages(&pages_to_delete); 36 + 37 + for &page_num in &new_order[1..] { 38 + let mut page_doc = doc.clone(); 39 + let pages_to_delete: Vec<u32> = all_pages.iter().filter(|&&p| p != page_num).copied().collect(); 40 + page_doc.delete_pages(&pages_to_delete); 41 + 42 + if let Some(&page_id) = page_doc.get_pages().get(&page_num) 43 + && let Ok(page_obj) = page_doc.get_object(page_id) 44 + { 45 + output_doc.add_object(page_obj.clone()); 46 + } 47 + } 48 + 49 + let mut merged = output_doc; 50 + 51 + let mut output = Vec::new(); 52 + merged 53 + .save_to(&mut output) 54 + .map_err(|e| io::Error::other(format!("Failed to save PDF: {}", e)))?; 55 + 56 + Ok(output) 57 + }
+74
crates/core/src/tools/rotate.rs
··· 1 + use lopdf::{Document, Object}; 2 + use std::io; 3 + 4 + /// Rotation angle in degrees (must be 0, 90, 180, or 270). 5 + #[derive(Debug, Clone, Copy)] 6 + pub enum Rotation { 7 + None = 0, 8 + Clockwise90 = 90, 9 + Clockwise180 = 180, 10 + Clockwise270 = 270, 11 + } 12 + 13 + impl Rotation { 14 + pub fn from_degrees(degrees: i32) -> io::Result<Self> { 15 + match degrees { 16 + 0 => Ok(Rotation::None), 17 + 90 => Ok(Rotation::Clockwise90), 18 + 180 => Ok(Rotation::Clockwise180), 19 + 270 => Ok(Rotation::Clockwise270), 20 + _ => Err(io::Error::new( 21 + io::ErrorKind::InvalidInput, 22 + format!("Invalid rotation angle: {}. Must be 0, 90, 180, or 270", degrees), 23 + )), 24 + } 25 + } 26 + } 27 + 28 + /// Rotates specific pages in a PDF document. 29 + /// 30 + /// Page numbers are 1-indexed. If page_numbers is empty, rotates all pages. 31 + /// Rotation angles must be 0, 90, 180, or 270 degrees. 32 + pub fn rotate_pages(input_file: &str, page_numbers: &[u32], rotation: Rotation) -> io::Result<Vec<u8>> { 33 + let mut doc = Document::load(input_file).map_err(|e| { 34 + io::Error::new( 35 + io::ErrorKind::InvalidData, 36 + format!("Failed to load PDF '{}': {}", input_file, e), 37 + ) 38 + })?; 39 + 40 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 41 + let max_page = *all_pages.iter().max().unwrap_or(&0); 42 + 43 + let pages_to_rotate = if page_numbers.is_empty() { 44 + all_pages 45 + } else { 46 + for &page_num in page_numbers { 47 + if page_num < 1 || page_num > max_page { 48 + return Err(io::Error::new( 49 + io::ErrorKind::InvalidInput, 50 + format!("Page {} is out of range (1-{})", page_num, max_page), 51 + )); 52 + } 53 + } 54 + page_numbers.to_vec() 55 + }; 56 + 57 + for page_num in pages_to_rotate { 58 + if let Some(&page_id) = doc.get_pages().get(&page_num) 59 + && let Ok(page_obj) = doc.get_object_mut(page_id) 60 + && let Object::Dictionary(dict) = page_obj 61 + { 62 + let current_rotation = dict.get(b"Rotate").and_then(|obj| obj.as_i64()).unwrap_or(0); 63 + 64 + let new_rotation = (current_rotation + rotation as i64) % 360; 65 + dict.set("Rotate", Object::Integer(new_rotation)); 66 + } 67 + } 68 + 69 + let mut output = Vec::new(); 70 + doc.save_to(&mut output) 71 + .map_err(|e| io::Error::other(format!("Failed to save PDF: {}", e)))?; 72 + 73 + Ok(output) 74 + }
+78
crates/core/src/tools/split.rs
··· 1 + use lopdf::Document; 2 + use std::io; 3 + 4 + /// Splits a PDF into separate single-page PDF files. 5 + /// 6 + /// Returns a vector of PDF bytes, one for each page in the input document. 7 + /// Each output PDF contains exactly one page from the original. 8 + pub fn split_pdf(input_file: &str) -> io::Result<Vec<Vec<u8>>> { 9 + let doc = Document::load(input_file).map_err(|e| { 10 + io::Error::new( 11 + io::ErrorKind::InvalidData, 12 + format!("Failed to load PDF '{}': {}", input_file, e), 13 + ) 14 + })?; 15 + 16 + let page_count = doc.get_pages().len(); 17 + let mut split_pdfs = Vec::new(); 18 + 19 + for page_num in 1..=page_count as u32 { 20 + let mut page_doc = doc.clone(); 21 + 22 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 23 + let pages_to_delete: Vec<u32> = all_pages.iter().filter(|&&p| p != page_num).copied().collect(); 24 + 25 + page_doc.delete_pages(&pages_to_delete); 26 + 27 + let mut output = Vec::new(); 28 + page_doc 29 + .save_to(&mut output) 30 + .map_err(|e| io::Error::other(format!("Failed to save page {}: {}", page_num, e)))?; 31 + 32 + split_pdfs.push(output); 33 + } 34 + 35 + Ok(split_pdfs) 36 + } 37 + 38 + /// Splits a PDF into chunks of specified page ranges. 39 + /// 40 + /// Each range is specified as a tuple (start_page, end_page) inclusive. 41 + /// Page numbers are 1-indexed. 42 + pub fn split_pdf_by_ranges(input_file: &str, ranges: &[(u32, u32)]) -> io::Result<Vec<Vec<u8>>> { 43 + let doc = Document::load(input_file).map_err(|e| { 44 + io::Error::new( 45 + io::ErrorKind::InvalidData, 46 + format!("Failed to load PDF '{}': {}", input_file, e), 47 + ) 48 + })?; 49 + 50 + let all_pages: Vec<u32> = doc.get_pages().keys().copied().collect(); 51 + let max_page = *all_pages.iter().max().unwrap_or(&0); 52 + 53 + let mut split_pdfs = Vec::new(); 54 + 55 + for (start, end) in ranges { 56 + if *start < 1 || *end > max_page || start > end { 57 + return Err(io::Error::new( 58 + io::ErrorKind::InvalidInput, 59 + format!("Invalid page range: {}-{}", start, end), 60 + )); 61 + } 62 + 63 + let mut range_doc = doc.clone(); 64 + 65 + let pages_to_delete: Vec<u32> = all_pages.iter().filter(|&&p| p < *start || p > *end).copied().collect(); 66 + 67 + range_doc.delete_pages(&pages_to_delete); 68 + 69 + let mut output = Vec::new(); 70 + range_doc 71 + .save_to(&mut output) 72 + .map_err(|e| io::Error::other(format!("Failed to save range {}-{}: {}", start, end, e)))?; 73 + 74 + split_pdfs.push(output); 75 + } 76 + 77 + Ok(split_pdfs) 78 + }