github.com/pdfcpu/pdfcpu@v0.11.1/pkg/cli/list.go (about) 1 /* 2 Copyright 2023 The pdfcpu Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package cli provides pdfcpu command line processing. 18 package cli 19 20 import ( 21 "crypto/x509" 22 "encoding/json" 23 "encoding/pem" 24 "fmt" 25 "io" 26 "math" 27 "os" 28 "path/filepath" 29 "sort" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/hhrutter/pkcs7" 35 "github.com/pdfcpu/pdfcpu/pkg/api" 36 "github.com/pdfcpu/pdfcpu/pkg/log" 37 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" 38 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" 39 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" 40 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" 41 "github.com/pkg/errors" 42 ) 43 44 func listAttachments(rs io.ReadSeeker, conf *model.Configuration, withDesc, sorted bool) ([]string, error) { 45 if rs == nil { 46 return nil, errors.New("pdfcpu: listAttachments: missing rs") 47 } 48 49 if conf == nil { 50 conf = model.NewDefaultConfiguration() 51 } 52 conf.Cmd = model.LISTATTACHMENTS 53 54 ctx, err := api.ReadAndValidate(rs, conf) 55 if err != nil { 56 return nil, err 57 } 58 59 aa, err := ctx.ListAttachments() 60 if err != nil { 61 return nil, err 62 } 63 64 var ss []string 65 for _, a := range aa { 66 s := a.FileName 67 if withDesc && a.Desc != "" { 68 s = fmt.Sprintf("%s (%s)", s, a.Desc) 69 } 70 ss = append(ss, s) 71 } 72 if sorted { 73 sort.Strings(ss) 74 } 75 76 return ss, nil 77 } 78 79 // ListAttachmentsFile returns a list of embedded file attachments of inFile with optional description. 80 func ListAttachmentsFile(inFile string, conf *model.Configuration) ([]string, error) { 81 f, err := os.Open(inFile) 82 if err != nil { 83 return nil, err 84 } 85 defer f.Close() 86 87 return listAttachments(f, conf, true, true) 88 } 89 90 // ListAttachmentsCompactFile returns a list of embedded file attachments of inFile w/o optional description. 91 func ListAttachmentsCompactFile(inFile string, conf *model.Configuration) ([]string, error) { 92 f, err := os.Open(inFile) 93 if err != nil { 94 return nil, err 95 } 96 defer f.Close() 97 98 return listAttachments(f, conf, false, false) 99 } 100 101 func listAnnotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (int, []string, error) { 102 annots, err := api.Annotations(rs, selectedPages, conf) 103 if err != nil { 104 return 0, nil, err 105 } 106 107 return pdfcpu.ListAnnotations(annots) 108 } 109 110 // ListAnnotationsFile returns a list of page annotations of inFile. 111 func ListAnnotationsFile(inFile string, selectedPages []string, conf *model.Configuration) (int, []string, error) { 112 f, err := os.Open(inFile) 113 if err != nil { 114 return 0, nil, err 115 } 116 defer f.Close() 117 118 return listAnnotations(f, selectedPages, conf) 119 } 120 121 func listBoxes(rs io.ReadSeeker, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { 122 if rs == nil { 123 return nil, errors.New("pdfcpu: listBoxes: missing rs") 124 } 125 126 if conf == nil { 127 conf = model.NewDefaultConfiguration() 128 } 129 conf.Cmd = model.LISTBOXES 130 131 ctx, err := api.ReadAndValidate(rs, conf) 132 if err != nil { 133 return nil, err 134 } 135 136 pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) 137 if err != nil { 138 return nil, err 139 } 140 141 return ctx.ListPageBoundaries(pages, pb) 142 } 143 144 // ListBoxesFile returns a list of page boundaries for selected pages of inFile. 145 func ListBoxesFile(inFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { 146 f, err := os.Open(inFile) 147 if err != nil { 148 return nil, err 149 } 150 defer f.Close() 151 152 if pb == nil { 153 pb = &model.PageBoundaries{} 154 pb.SelectAll() 155 } 156 log.CLI.Printf("listing %s for %s\n", pb, inFile) 157 158 return listBoxes(f, selectedPages, pb, conf) 159 } 160 161 func listFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { 162 if conf == nil { 163 conf = model.NewDefaultConfiguration() 164 } 165 conf.Cmd = model.LISTFORMFIELDS 166 167 ctx, err := api.ReadAndValidate(rs, conf) 168 if err != nil { 169 return nil, err 170 } 171 172 return form.ListFormFields(ctx) 173 } 174 175 // ListFormFieldsFile returns a list of form field ids in inFile. 176 func ListFormFieldsFile(inFiles []string, conf *model.Configuration) ([]string, error) { 177 log.SetCLILogger(nil) 178 179 ss := []string{} 180 181 for _, fn := range inFiles { 182 183 f, err := os.Open(fn) 184 if err != nil { 185 if len(inFiles) > 1 { 186 ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) 187 continue 188 } 189 return nil, err 190 } 191 defer f.Close() 192 193 output, err := listFormFields(f, conf) 194 if err != nil { 195 if len(inFiles) > 1 { 196 ss = append(ss, fmt.Sprintf("\n%s:\n%v", fn, err)) 197 continue 198 } 199 return nil, err 200 } 201 202 ss = append(ss, "\n"+fn+":\n") 203 ss = append(ss, output...) 204 } 205 206 return ss, nil 207 } 208 209 func listImages(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) { 210 if rs == nil { 211 return nil, errors.New("pdfcpu: listImages: Please provide rs") 212 } 213 214 if conf == nil { 215 conf = model.NewDefaultConfiguration() 216 } 217 conf.Cmd = model.LISTIMAGES 218 219 ctx, err := api.ReadValidateAndOptimize(rs, conf) 220 if err != nil { 221 return nil, err 222 } 223 224 pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) 225 if err != nil { 226 return nil, err 227 } 228 229 return pdfcpu.ListImages(ctx, pages) 230 } 231 232 // ListImagesFile returns a formatted list of embedded images of inFile. 233 func ListImagesFile(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { 234 if len(selectedPages) == 0 { 235 log.CLI.Printf("pages: all\n") 236 } 237 238 log.SetCLILogger(nil) 239 240 ss := []string{} 241 242 for _, fn := range inFiles { 243 f, err := os.Open(fn) 244 if err != nil { 245 if len(inFiles) > 1 { 246 ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) 247 continue 248 } 249 return nil, err 250 } 251 defer f.Close() 252 output, err := listImages(f, selectedPages, conf) 253 if err != nil { 254 if len(inFiles) > 1 { 255 ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) 256 continue 257 } 258 return nil, err 259 } 260 ss = append(ss, "\n"+fn+":") 261 ss = append(ss, output...) 262 } 263 264 return ss, nil 265 } 266 267 // ListInfoFile returns formatted information about inFile. 268 func ListInfoFile(inFile string, selectedPages []string, fonts bool, conf *model.Configuration) ([]string, error) { 269 f, err := os.Open(inFile) 270 if err != nil { 271 return nil, err 272 } 273 defer f.Close() 274 275 info, err := api.PDFInfo(f, inFile, selectedPages, fonts, conf) 276 if err != nil { 277 return nil, err 278 } 279 280 pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false) 281 if err != nil { 282 return nil, err 283 } 284 285 ss, err := pdfcpu.ListInfo(info, pages, fonts) 286 if err != nil { 287 return nil, err 288 } 289 290 return append([]string{inFile + ":"}, ss...), err 291 } 292 293 func jsonInfo(info *pdfcpu.PDFInfo, pages types.IntSet) (map[string]model.PageBoundaries, []types.Dim) { 294 if len(pages) > 0 { 295 pbs := map[string]model.PageBoundaries{} 296 for i, pb := range info.PageBoundaries { 297 if _, found := pages[i+1]; !found { 298 continue 299 } 300 d := pb.CropBox().Dimensions() 301 if pb.Rot%180 != 0 { 302 d.Width, d.Height = d.Height, d.Width 303 } 304 pb.Orientation = "portrait" 305 if d.Landscape() { 306 pb.Orientation = "landscape" 307 } 308 if pb.Media != nil { 309 pb.Media.Rect = pb.Media.Rect.ConvertToUnit(info.Unit) 310 pb.Media.Rect.LL.X = math.Round(pb.Media.Rect.LL.X*100) / 100 311 pb.Media.Rect.LL.Y = math.Round(pb.Media.Rect.LL.Y*100) / 100 312 pb.Media.Rect.UR.X = math.Round(pb.Media.Rect.UR.X*100) / 100 313 pb.Media.Rect.UR.Y = math.Round(pb.Media.Rect.UR.Y*100) / 100 314 } 315 if pb.Crop != nil { 316 pb.Crop.Rect = pb.Crop.Rect.ConvertToUnit(info.Unit) 317 pb.Crop.Rect.LL.X = math.Round(pb.Crop.Rect.LL.X*100) / 100 318 pb.Crop.Rect.LL.Y = math.Round(pb.Crop.Rect.LL.Y*100) / 100 319 pb.Crop.Rect.UR.X = math.Round(pb.Crop.Rect.UR.X*100) / 100 320 pb.Crop.Rect.UR.Y = math.Round(pb.Crop.Rect.UR.Y*100) / 100 321 } 322 if pb.Trim != nil { 323 pb.Trim.Rect = pb.Trim.Rect.ConvertToUnit(info.Unit) 324 pb.Trim.Rect.LL.X = math.Round(pb.Trim.Rect.LL.X*100) / 100 325 pb.Trim.Rect.LL.Y = math.Round(pb.Trim.Rect.LL.Y*100) / 100 326 pb.Trim.Rect.UR.X = math.Round(pb.Trim.Rect.UR.X*100) / 100 327 pb.Trim.Rect.UR.Y = math.Round(pb.Trim.Rect.UR.Y*100) / 100 328 } 329 if pb.Bleed != nil { 330 pb.Bleed.Rect = pb.Bleed.Rect.ConvertToUnit(info.Unit) 331 pb.Bleed.Rect.LL.X = math.Round(pb.Bleed.Rect.LL.X*100) / 100 332 pb.Bleed.Rect.LL.Y = math.Round(pb.Bleed.Rect.LL.Y*100) / 100 333 pb.Bleed.Rect.UR.X = math.Round(pb.Bleed.Rect.UR.X*100) / 100 334 pb.Bleed.Rect.UR.Y = math.Round(pb.Bleed.Rect.UR.Y*100) / 100 335 } 336 if pb.Art != nil { 337 pb.Art.Rect = pb.Art.Rect.ConvertToUnit(info.Unit) 338 pb.Art.Rect.LL.X = math.Round(pb.Art.Rect.LL.X*100) / 100 339 pb.Art.Rect.LL.Y = math.Round(pb.Art.Rect.LL.Y*100) / 100 340 pb.Art.Rect.UR.X = math.Round(pb.Art.Rect.UR.X*100) / 100 341 pb.Art.Rect.UR.Y = math.Round(pb.Art.Rect.UR.Y*100) / 100 342 } 343 pbs[strconv.Itoa(i+1)] = pb 344 } 345 return pbs, nil 346 } 347 348 var dims []types.Dim 349 for k, v := range info.PageDimensions { 350 if v { 351 dc := k.ConvertToUnit(info.Unit) 352 dc.Width = math.Round(dc.Width*100) / 100 353 dc.Height = math.Round(dc.Height*100) / 100 354 dims = append(dims, dc) 355 } 356 } 357 return nil, dims 358 } 359 360 func listInfoFilesJSON(inFiles []string, selectedPages []string, fonts bool, conf *model.Configuration) ([]string, error) { 361 var infos []*pdfcpu.PDFInfo 362 363 for _, fn := range inFiles { 364 365 f, err := os.Open(fn) 366 if err != nil { 367 return nil, err 368 } 369 defer f.Close() 370 371 info, err := api.PDFInfo(f, fn, selectedPages, fonts, conf) 372 if err != nil { 373 return nil, err 374 } 375 376 pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false) 377 if err != nil { 378 return nil, err 379 } 380 381 info.Boundaries, info.Dimensions = jsonInfo(info, pages) 382 383 infos = append(infos, info) 384 } 385 386 s := struct { 387 Header pdfcpu.Header `json:"header"` 388 Infos []*pdfcpu.PDFInfo `json:"infos"` 389 }{ 390 Header: pdfcpu.Header{Version: "pdfcpu " + model.VersionStr, Creation: time.Now().Format("2006-01-02 15:04:05 MST")}, 391 Infos: infos, 392 } 393 394 bb, err := json.MarshalIndent(s, "", "\t") 395 if err != nil { 396 return nil, err 397 } 398 399 return []string{string(bb)}, nil 400 } 401 402 // ListInfoFiles returns formatted information about inFiles. 403 func ListInfoFiles(inFiles []string, selectedPages []string, fonts, json bool, conf *model.Configuration) ([]string, error) { 404 405 if json { 406 return listInfoFilesJSON(inFiles, selectedPages, fonts, conf) 407 } 408 409 var ss []string 410 411 for i, fn := range inFiles { 412 if i > 0 { 413 ss = append(ss, "") 414 } 415 ssx, err := ListInfoFile(fn, selectedPages, fonts, conf) 416 if err != nil { 417 if len(inFiles) == 1 { 418 return nil, err 419 } 420 fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) 421 } 422 ss = append(ss, ssx...) 423 } 424 425 return ss, nil 426 } 427 428 // ListKeywordsFile returns the keyword list of inFile. 429 func ListKeywordsFile(inFile string, conf *model.Configuration) ([]string, error) { 430 f, err := os.Open(inFile) 431 if err != nil { 432 return nil, err 433 } 434 defer f.Close() 435 436 return api.Keywords(f, conf) 437 } 438 439 func listPermissions(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { 440 if rs == nil { 441 return nil, errors.New("pdfcpu: listPermissions: missing rs") 442 } 443 444 if conf == nil { 445 conf = model.NewDefaultConfiguration() 446 } 447 conf.Cmd = model.LISTPERMISSIONS 448 449 ctx, err := api.ReadAndValidate(rs, conf) 450 if err != nil { 451 return nil, err 452 } 453 454 return pdfcpu.Permissions(ctx), nil 455 } 456 457 // ListPermissionsFile returns a list of user access permissions for inFile. 458 func ListPermissionsFile(inFiles []string, conf *model.Configuration) ([]string, error) { 459 log.SetCLILogger(nil) 460 461 var ss []string 462 463 for i, fn := range inFiles { 464 if i > 0 { 465 ss = append(ss, "") 466 } 467 f, err := os.Open(fn) 468 if err != nil { 469 return nil, err 470 } 471 defer func() { 472 f.Close() 473 }() 474 ssx, err := listPermissions(f, conf) 475 if err != nil { 476 if len(inFiles) == 1 { 477 return nil, err 478 } 479 fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) 480 } 481 ss = append(ss, fn+":") 482 ss = append(ss, ssx...) 483 } 484 485 return ss, nil 486 } 487 488 func listProperties(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { 489 if rs == nil { 490 return nil, errors.New("pdfcpu: listProperties: missing rs") 491 } 492 493 if conf == nil { 494 conf = model.NewDefaultConfiguration() 495 } else { 496 conf.ValidationMode = model.ValidationRelaxed 497 } 498 conf.Cmd = model.LISTPROPERTIES 499 500 ctx, err := api.ReadAndValidate(rs, conf) 501 if err != nil { 502 return nil, err 503 } 504 505 return pdfcpu.PropertiesList(ctx) 506 } 507 508 // ListPropertiesFile returns the property list of inFile. 509 func ListPropertiesFile(inFile string, conf *model.Configuration) ([]string, error) { 510 f, err := os.Open(inFile) 511 if err != nil { 512 return nil, err 513 } 514 defer f.Close() 515 516 return listProperties(f, conf) 517 } 518 519 func listBookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { 520 if rs == nil { 521 return nil, errors.New("pdfcpu: listBookmarks: missing rs") 522 } 523 524 if conf == nil { 525 conf = model.NewDefaultConfiguration() 526 } else { 527 conf.ValidationMode = model.ValidationRelaxed 528 } 529 conf.Cmd = model.LISTBOOKMARKS 530 531 ctx, err := api.ReadAndValidate(rs, conf) 532 if err != nil { 533 return nil, err 534 } 535 536 return pdfcpu.BookmarkList(ctx) 537 } 538 539 // ListBookmarksFile returns the bookmarks of inFile. 540 func ListBookmarksFile(inFile string, conf *model.Configuration) ([]string, error) { 541 f, err := os.Open(inFile) 542 if err != nil { 543 return nil, err 544 } 545 defer f.Close() 546 547 return listBookmarks(f, conf) 548 } 549 550 func listPEM(fName string) (int, error) { 551 bb, err := os.ReadFile(fName) 552 if err != nil { 553 fmt.Printf("%v\n", err) 554 return 0, err 555 } 556 557 if len(bb) == 0 { 558 //return 0, errors.Errorf("%s is empty\n", filepath.Base(fName)) 559 return 0, errors.New("is empty\n") 560 } 561 562 ss := []string{} 563 for len(bb) > 0 { 564 var block *pem.Block 565 block, bb = pem.Decode(bb) 566 if block == nil { 567 break 568 } 569 if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { 570 continue 571 } 572 573 certBytes := block.Bytes 574 cert, err := x509.ParseCertificate(certBytes) 575 if err != nil { 576 fmt.Printf("%v\n", err) 577 continue 578 } 579 ss = append(ss, model.CertString(cert)) 580 } 581 582 sort.Strings(ss) 583 for i, s := range ss { 584 fmt.Printf("%03d:\n%s\n", i+1, s) 585 } 586 587 return len(ss), nil 588 } 589 590 func listP7C(fName string) (int, error) { 591 bb, err := os.ReadFile(fName) 592 if err != nil { 593 fmt.Printf("%v\n", err) 594 return 0, err 595 } 596 597 if len(bb) == 0 { 598 //return 0, errors.Errorf("%s is empty\n", filepath.Base(fName)) 599 return 0, errors.New("is empty\n") 600 } 601 602 // // Check if the data starts with PEM markers (for Base64 encoding) 603 // if isPEM(data) { 604 // // If the file is Base64 encoded (PEM format), decode it 605 // decodedData, err := base64.StdEncoding.DecodeString(string(data)) 606 // if err != nil { 607 // log.Fatalf("Error decoding Base64: %v", err) 608 // } 609 // data = decodedData 610 // } 611 612 p7, err := pkcs7.Parse(bb) 613 if err != nil { 614 return 0, err 615 } 616 617 ss := []string{} 618 for _, cert := range p7.Certificates { 619 ss = append(ss, model.CertString(cert)) 620 } 621 622 sort.Strings(ss) 623 for i, s := range ss { 624 fmt.Printf("%03d:\n%s\n", i+1, s) 625 } 626 627 return len(ss), nil 628 } 629 630 // ListCertificatesAll returns formatted information about installed certificates. 631 func ListCertificatesAll(json bool, conf *model.Configuration) ([]string, error) { 632 // Process *.pem and *.p7c 633 fmt.Printf("certDir: %s\n", model.CertDir) 634 635 if err := os.MkdirAll(model.CertDir, os.ModePerm); err != nil { 636 return nil, err 637 } 638 639 count := 0 640 641 err := filepath.WalkDir(model.CertDir, func(path string, d os.DirEntry, err error) error { 642 if err != nil { 643 return err 644 } 645 if d.IsDir() { 646 return nil 647 } 648 if !model.IsPEM(path) && !model.IsP7C(path) { 649 return nil 650 } 651 652 fmt.Printf("\n%s:\n", strings.TrimPrefix(path, model.CertDir)) 653 654 if model.IsPEM(path) { 655 c, err := listPEM(path) 656 if err != nil { 657 fmt.Printf("%v\n", err) 658 } 659 count += c 660 return nil 661 } 662 c, err := listP7C(path) 663 if err != nil { 664 fmt.Printf("%v\n", err) 665 } 666 count += c 667 return nil 668 }) 669 670 fmt.Printf("total installed certs: %d\n", count) 671 672 return nil, err 673 }