From 9280f26f30981fc093deb283930b9c8b07117af2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:31:24 +0000 Subject: [PATCH 1/7] style: gofmt -s --- encrypt/encrypt.go | 2 +- gpdf.go | 2 +- pdfa/icc.go | 20 ++++++++++---------- signature/byterange.go | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/encrypt/encrypt.go b/encrypt/encrypt.go index 87ce351..4b7128b 100644 --- a/encrypt/encrypt.go +++ b/encrypt/encrypt.go @@ -94,7 +94,7 @@ func Apply(pw *pdf.Writer, opts ...Option) error { pdf.Name("Filter"): pdf.Name("Standard"), pdf.Name("V"): pdf.Integer(5), pdf.Name("R"): pdf.Integer(6), - pdf.Name("Length"): pdf.Integer(256), + pdf.Name("Length"): pdf.Integer(256), pdf.Name("CF"): pdf.Dict{ pdf.Name("StdCF"): pdf.Dict{ pdf.Name("Type"): pdf.Name("CryptFilter"), diff --git a/gpdf.go b/gpdf.go index fb3c537..7587563 100644 --- a/gpdf.go +++ b/gpdf.go @@ -26,8 +26,8 @@ import ( "github.com/gpdf-dev/gpdf/document" "github.com/gpdf-dev/gpdf/encrypt" - "github.com/gpdf-dev/gpdf/pdfa" "github.com/gpdf-dev/gpdf/pdf" + "github.com/gpdf-dev/gpdf/pdfa" "github.com/gpdf-dev/gpdf/signature" "github.com/gpdf-dev/gpdf/template" ) diff --git a/pdfa/icc.go b/pdfa/icc.go index d94948b..00e85d9 100644 --- a/pdfa/icc.go +++ b/pdfa/icc.go @@ -72,8 +72,8 @@ func buildSRGBProfile() []byte { _ = binary.Write(&buf, binary.BigEndian, uint32(0)) // unicode count // ScriptCode (empty) _ = binary.Write(&buf, binary.BigEndian, uint16(0)) // scriptcode code - buf.WriteByte(0) // scriptcode count - buf.Write(make([]byte, 67)) // scriptcode string (67 bytes) + buf.WriteByte(0) // scriptcode count + buf.Write(make([]byte, 67)) // scriptcode string (67 bytes) // Pad to 4-byte boundary for buf.Len()%4 != 0 { buf.WriteByte(0) @@ -135,7 +135,7 @@ func buildSRGBProfile() []byte { buf.Grow(totalSize) // === Header (128 bytes) === - _ = binary.Write(buf, binary.BigEndian, uint32(totalSize)) // Profile size + _ = binary.Write(buf, binary.BigEndian, uint32(totalSize)) // Profile size buf.WriteString("gpdf") // Preferred CMM Type _ = binary.Write(buf, binary.BigEndian, uint32(0x02100000)) // Version 2.1.0 buf.WriteString("mntr") // Device class: monitor @@ -148,13 +148,13 @@ func buildSRGBProfile() []byte { _ = binary.Write(buf, binary.BigEndian, uint16(0)) // hour _ = binary.Write(buf, binary.BigEndian, uint16(0)) // minute _ = binary.Write(buf, binary.BigEndian, uint16(0)) // second - buf.WriteString("acsp") // File signature (always "acsp") - buf.WriteString("APPL") // Primary platform - _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Profile flags - buf.WriteString("gpdf") // Device manufacturer - buf.WriteString("sRGB") // Device model - _ = binary.Write(buf, binary.BigEndian, uint64(0)) // Device attributes - _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Rendering intent (perceptual) + buf.WriteString("acsp") // File signature (always "acsp") + buf.WriteString("APPL") // Primary platform + _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Profile flags + buf.WriteString("gpdf") // Device manufacturer + buf.WriteString("sRGB") // Device model + _ = binary.Write(buf, binary.BigEndian, uint64(0)) // Device attributes + _ = binary.Write(buf, binary.BigEndian, uint32(0)) // Rendering intent (perceptual) // PCS illuminant (D50 XYZ) _ = binary.Write(buf, binary.BigEndian, s15Fixed16(0.9642)) _ = binary.Write(buf, binary.BigEndian, s15Fixed16(1.0)) diff --git a/signature/byterange.go b/signature/byterange.go index 0495954..0c83ded 100644 --- a/signature/byterange.go +++ b/signature/byterange.go @@ -112,8 +112,8 @@ func buildSignedPDF(pdfData []byte, signer Signer, cfg *signConfig) (*signResult br := [4]int64{ 0, - int64(sigStart) - 1, // up to and excluding '<' - int64(sigEnd) + 1, // after '>' + int64(sigStart) - 1, // up to and excluding '<' + int64(sigEnd) + 1, // after '>' int64(len(result)) - int64(sigEnd) - 1, } From 4c8e8e95ee44f4c20d7e6f5141238e4bcdee33a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:32:36 +0000 Subject: [PATCH 2/7] update coverage badge to 85.3% --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed58fae..61ef553 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/gpdf-dev/gpdf.svg)](https://pkg.go.dev/github.com/gpdf-dev/gpdf) [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) -![coverage](https://img.shields.io/badge/coverage-92.6%25-brightgreen) +![coverage](https://img.shields.io/badge/coverage-85.3%25-green) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) From 3927fc075762a6270a96d88bae79447f17db2cec Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Sat, 21 Mar 2026 23:37:52 +0900 Subject: [PATCH 3/7] docs: add gpdf.dev website badge to all README files --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- README.md | 1 + README_es.md | 1 + README_ja.md | 1 + README_ko.md | 1 + README_pt.md | 1 + README_zh.md | 1 + _benchmark/go.mod | 2 +- 8 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ad625d3..03d520c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -63,7 +63,7 @@ body: id: gpdf-version attributes: label: gpdf Version - placeholder: "v0.9.2" + placeholder: "v1.0.0" validations: required: true diff --git a/README.md b/README.md index 61ef553..fa79a2e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ![coverage](https://img.shields.io/badge/coverage-85.3%25-green) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) **English** | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md) diff --git a/README_es.md b/README_es.md index c7341a4..c2e3322 100644 --- a/README_es.md +++ b/README_es.md @@ -4,6 +4,7 @@ [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) [English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | **Español** | [Português](README_pt.md) diff --git a/README_ja.md b/README_ja.md index 83b054c..48706d9 100644 --- a/README_ja.md +++ b/README_ja.md @@ -4,6 +4,7 @@ [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) [English](README.md) | **日本語** | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md) diff --git a/README_ko.md b/README_ko.md index 5f2f010..bce68af 100644 --- a/README_ko.md +++ b/README_ko.md @@ -4,6 +4,7 @@ [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) [English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | **한국어** | [Español](README_es.md) | [Português](README_pt.md) diff --git a/README_pt.md b/README_pt.md index 4d2c08c..dddef76 100644 --- a/README_pt.md +++ b/README_pt.md @@ -4,6 +4,7 @@ [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) [English](README.md) | [日本語](README_ja.md) | [中文](README_zh.md) | [한국어](README_ko.md) | [Español](README_es.md) | **Português** diff --git a/README_zh.md b/README_zh.md index a306cd6..69dfa67 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,6 +4,7 @@ [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) +[![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/) [English](README.md) | [日本語](README_ja.md) | **中文** | [한국어](README_ko.md) | [Español](README_es.md) | [Português](README_pt.md) diff --git a/_benchmark/go.mod b/_benchmark/go.mod index 9048977..f6e2c8b 100644 --- a/_benchmark/go.mod +++ b/_benchmark/go.mod @@ -7,7 +7,7 @@ replace github.com/gpdf-dev/gpdf => ../ require ( github.com/go-pdf/fpdf v0.9.0 github.com/johnfercher/maroto/v2 v2.3.3 - github.com/gpdf-dev/gpdf v0.9.2 + github.com/gpdf-dev/gpdf v1.0.0 github.com/signintech/gopdf v0.36.0 golang.org/x/image v0.18.0 ) From 30b4a0da53d6837a70a16c43dd1cc23aa2d5ec08 Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Sat, 21 Mar 2026 23:45:45 +0900 Subject: [PATCH 4/7] chore: update version references to v1.0.0 --- CHANGELOG.md | 4 ++-- _benchmark/go.mod | 2 +- _benchmark/go.sum | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3b493..2f6b9e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,8 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Reed-Solomon coefficient order in QR code encoder - binary.Write return value handling for errcheck lint -[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v0.9.0...HEAD -[0.9.0]: https://github.com/gpdf-dev/gpdf/compare/v0.8.0...v0.9.0 +[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.0...HEAD +[0.9.0]: https://github.com/gpdf-dev/gpdf/compare/v0.8.0...v1.0.0 [0.8.0]: https://github.com/gpdf-dev/gpdf/compare/v0.5.0...v0.8.0 [0.7.0]: https://github.com/gpdf-dev/gpdf/releases/tag/v0.7.0 [0.6.0]: https://github.com/gpdf-dev/gpdf/releases/tag/v0.6.0 diff --git a/_benchmark/go.mod b/_benchmark/go.mod index f6e2c8b..b3300a6 100644 --- a/_benchmark/go.mod +++ b/_benchmark/go.mod @@ -5,7 +5,7 @@ go 1.24 replace github.com/gpdf-dev/gpdf => ../ require ( - github.com/go-pdf/fpdf v0.9.0 + github.com/go-pdf/fpdf v1.0.0 github.com/johnfercher/maroto/v2 v2.3.3 github.com/gpdf-dev/gpdf v1.0.0 github.com/signintech/gopdf v0.36.0 diff --git a/_benchmark/go.sum b/_benchmark/go.sum index 3dba72d..62c9eed 100644 --- a/_benchmark/go.sum +++ b/_benchmark/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg= github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U= -github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= -github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= +github.com/go-pdf/fpdf v1.0.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= +github.com/go-pdf/fpdf v1.0.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= From 27285ba88129f3d19468aeaf3fdbf928783c8b87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:47:44 +0000 Subject: [PATCH 5/7] chore: bump version to v1.0.0 --- internal/buildinfo/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go index e9cc0f6..91ec9d3 100644 --- a/internal/buildinfo/version.go +++ b/internal/buildinfo/version.go @@ -3,4 +3,4 @@ package buildinfo // Version is the library version. It is the single source of truth used by // the public gpdf.Version constant and the default PDF Producer metadata. -const Version = "0.9.2" +const Version = "1.0.0" From 4b5d7ad19ddedbefed9ed8d8e66bebd59e4438da Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Sat, 21 Mar 2026 23:58:51 +0900 Subject: [PATCH 6/7] fix: resolve lint errors in signature and pdfa packages --- pdfa/verify.go | 200 ++++++++++++++++++++++++++------------------ signature/cms.go | 114 +++++++++++++------------ signature/verify.go | 2 +- 3 files changed, 179 insertions(+), 137 deletions(-) diff --git a/pdfa/verify.go b/pdfa/verify.go index 676f66a..e78a806 100644 --- a/pdfa/verify.go +++ b/pdfa/verify.go @@ -86,38 +86,49 @@ func ParseConformance(pdfData []byte) *Conformance { // Validate checks all PDF/A conformance requirements and returns a list of violations. func (c *Conformance) Validate() []string { var violations []string + violations = c.validateXMP(violations) + violations = c.validateOutputIntents(violations) + violations = c.validateICC(violations) - // XMP checks + for _, elem := range c.ForbiddenElements { + violations = append(violations, fmt.Sprintf("forbidden element found: %s", elem)) + } + + return violations +} + +func (c *Conformance) validateXMP(violations []string) []string { if c.XMP == nil { - violations = append(violations, "XMP metadata not found") - } else { - if !c.XMP.HasXPacket { - violations = append(violations, "XMP missing xpacket header/footer") - } - if c.Part == 0 { - violations = append(violations, "pdfaid:part not found or invalid") - } - if c.Level == "" { - violations = append(violations, "pdfaid:conformance not found") - } - if c.XMP.Title == "" { - violations = append(violations, "XMP dc:title is empty (recommended)") - } - if c.XMP.Producer == "" { - violations = append(violations, "XMP pdf:Producer is empty") - } - if c.XMP.CreatorTool == "" { - violations = append(violations, "XMP xmp:CreatorTool is empty") - } - if c.XMP.CreateDate == "" { - violations = append(violations, "XMP xmp:CreateDate is empty") - } - if c.XMP.ModifyDate == "" { - violations = append(violations, "XMP xmp:ModifyDate is empty") - } + return append(violations, "XMP metadata not found") + } + if !c.XMP.HasXPacket { + violations = append(violations, "XMP missing xpacket header/footer") + } + if c.Part == 0 { + violations = append(violations, "pdfaid:part not found or invalid") + } + if c.Level == "" { + violations = append(violations, "pdfaid:conformance not found") + } + if c.XMP.Title == "" { + violations = append(violations, "XMP dc:title is empty (recommended)") } + if c.XMP.Producer == "" { + violations = append(violations, "XMP pdf:Producer is empty") + } + if c.XMP.CreatorTool == "" { + violations = append(violations, "XMP xmp:CreatorTool is empty") + } + if c.XMP.CreateDate == "" { + violations = append(violations, "XMP xmp:CreateDate is empty") + } + if c.XMP.ModifyDate == "" { + violations = append(violations, "XMP xmp:ModifyDate is empty") + } + return violations +} - // OutputIntent checks +func (c *Conformance) validateOutputIntents(violations []string) []string { if !c.HasOutputIntents { violations = append(violations, "/OutputIntents missing from catalog") } @@ -133,33 +144,28 @@ func (c *Conformance) Validate() []string { if !c.HasMetadataStream { violations = append(violations, "Metadata stream (/Subtype /XML) missing") } + return violations +} - // ICC profile checks +func (c *Conformance) validateICC(violations []string) []string { if c.ICCInfo == nil { - violations = append(violations, "ICC color profile not found") - } else { - if !c.ICCInfo.HasACSP { - violations = append(violations, "ICC profile missing 'acsp' signature") - } - if c.ICCInfo.ColorSpace != "RGB " { - violations = append(violations, fmt.Sprintf("ICC color space = %q, want 'RGB '", c.ICCInfo.ColorSpace)) - } - if c.ICCInfo.DeviceClass != "mntr" { - violations = append(violations, fmt.Sprintf("ICC device class = %q, want 'mntr'", c.ICCInfo.DeviceClass)) - } - if c.Part == 1 && !strings.HasPrefix(c.ICCInfo.Version, "2.") { - violations = append(violations, fmt.Sprintf("PDF/A-1b requires ICC v2.x, got %s", c.ICCInfo.Version)) - } - for _, tag := range c.ICCInfo.MissingTags { - violations = append(violations, fmt.Sprintf("ICC profile missing required tag: %s", tag)) - } + return append(violations, "ICC color profile not found") } - - // Forbidden elements - for _, elem := range c.ForbiddenElements { - violations = append(violations, fmt.Sprintf("forbidden element found: %s", elem)) + if !c.ICCInfo.HasACSP { + violations = append(violations, "ICC profile missing 'acsp' signature") + } + if c.ICCInfo.ColorSpace != "RGB " { + violations = append(violations, fmt.Sprintf("ICC color space = %q, want 'RGB '", c.ICCInfo.ColorSpace)) + } + if c.ICCInfo.DeviceClass != "mntr" { + violations = append(violations, fmt.Sprintf("ICC device class = %q, want 'mntr'", c.ICCInfo.DeviceClass)) + } + if c.Part == 1 && !strings.HasPrefix(c.ICCInfo.Version, "2.") { + violations = append(violations, fmt.Sprintf("PDF/A-1b requires ICC v2.x, got %s", c.ICCInfo.Version)) + } + for _, tag := range c.ICCInfo.MissingTags { + violations = append(violations, fmt.Sprintf("ICC profile missing required tag: %s", tag)) } - return violations } @@ -212,7 +218,21 @@ func parseXMP(pdfStr string) (*XMPInfo, error) { // Extract XMP between xpacket markers xmpRegion := pdfStr[beginIdx : endIdx+len("")] - // Find ... + rdf, err := decodeRDF(xmpRegion) + if err != nil { + return nil, err + } + + // Extract values from all Description elements + for _, desc := range rdf.Descriptions { + mergeDescription(info, &desc) + } + + return info, nil +} + +// decodeRDF extracts and decodes the rdf:RDF element from an XMP region. +func decodeRDF(xmpRegion string) (*xmpRDF, error) { rdfStart := strings.Index(xmpRegion, "") if rdfStart < 0 || rdfEnd < 0 { @@ -220,45 +240,59 @@ func parseXMP(pdfStr string) (*XMPInfo, error) { } rdfXML := xmpRegion[rdfStart : rdfEnd+len("")] - // Parse with xml.Decoder var rdf xmpRDF decoder := xml.NewDecoder(strings.NewReader(rdfXML)) if err := decoder.Decode(&rdf); err != nil { return nil, fmt.Errorf("XML decode: %w", err) } + return &rdf, nil +} - // Extract values from all Description elements - for _, desc := range rdf.Descriptions { - if desc.Title != nil && len(desc.Title.Items) > 0 && desc.Title.Items[0].Value != "" { - info.Title = desc.Title.Items[0].Value - } - if desc.Creator != nil && len(desc.Creator.Items) > 0 && desc.Creator.Items[0].Value != "" { - info.Creator = desc.Creator.Items[0].Value - } - if desc.Description != nil && len(desc.Description.Items) > 0 && desc.Description.Items[0].Value != "" { - info.Subject = desc.Description.Items[0].Value - } - if desc.CreatorTool != "" { - info.CreatorTool = desc.CreatorTool - } - if desc.CreateDate != "" { - info.CreateDate = desc.CreateDate - } - if desc.ModifyDate != "" { - info.ModifyDate = desc.ModifyDate - } - if desc.Producer != "" { - info.Producer = desc.Producer - } - if desc.Part > 0 { - info.PDFAPart = desc.Part - } - if desc.Conformance != "" { - info.PDFAConf = desc.Conformance - } +// altValue returns the first non-empty value from an xmpAlt, or "". +func altValue(a *xmpAlt) string { + if a != nil && len(a.Items) > 0 { + return a.Items[0].Value } + return "" +} - return info, nil +// seqValue returns the first non-empty value from an xmpSeq, or "". +func seqValue(s *xmpSeq) string { + if s != nil && len(s.Items) > 0 { + return s.Items[0].Value + } + return "" +} + +// mergeDescription merges fields from an xmpDescription into XMPInfo. +func mergeDescription(info *XMPInfo, desc *xmpDescription) { + if v := altValue(desc.Title); v != "" { + info.Title = v + } + if v := seqValue(desc.Creator); v != "" { + info.Creator = v + } + if v := altValue(desc.Description); v != "" { + info.Subject = v + } + if desc.CreatorTool != "" { + info.CreatorTool = desc.CreatorTool + } + if desc.CreateDate != "" { + info.CreateDate = desc.CreateDate + } + if desc.ModifyDate != "" { + info.ModifyDate = desc.ModifyDate + } + if desc.Producer != "" { + info.Producer = desc.Producer + } + if desc.Part > 0 { + info.PDFAPart = desc.Part + } + if desc.Conformance != "" { + info.PDFAConf = desc.Conformance + } } // --- ICC profile parsing --- diff --git a/signature/cms.go b/signature/cms.go index 6a6e419..9868979 100644 --- a/signature/cms.go +++ b/signature/cms.go @@ -63,68 +63,34 @@ type attribute struct { Values asn1.RawValue `asn1:"set"` } -// createCMSSignature creates a CMS/PKCS#7 SignedData structure. -func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, error) { - cert := signer.Certificate - - // Determine signature algorithm - var sigAlg pkix.AlgorithmIdentifier - switch signer.PrivateKey.(type) { +// signatureAlgorithm returns the AlgorithmIdentifier for the given private key. +func signatureAlgorithm(key crypto.PrivateKey) (pkix.AlgorithmIdentifier, error) { + switch key.(type) { case *rsa.PrivateKey: - sigAlg = pkix.AlgorithmIdentifier{Algorithm: oidRSAWithSHA256} + return pkix.AlgorithmIdentifier{Algorithm: oidRSAWithSHA256}, nil case *ecdsa.PrivateKey: - sigAlg = pkix.AlgorithmIdentifier{Algorithm: oidECDSAWithSHA256} + return pkix.AlgorithmIdentifier{Algorithm: oidECDSAWithSHA256}, nil default: - return nil, fmt.Errorf("unsupported key type: %T", signer.PrivateKey) - } - - digestAlg := pkix.AlgorithmIdentifier{Algorithm: oidSHA256} - - // Build signed attributes - signTime := cfg.signTime - if signTime.IsZero() { - signTime = time.Now() - } - - attrs, err := buildSignedAttrs(hash, signTime) - if err != nil { - return nil, fmt.Errorf("build signed attrs: %w", err) - } - - // Marshal signed attributes as a SEQUENCE (the default for []attribute) - attrsBytes, err := marshalAttributes(attrs) - if err != nil { - return nil, fmt.Errorf("marshal attrs: %w", err) + return pkix.AlgorithmIdentifier{}, fmt.Errorf("unsupported key type: %T", key) } +} - // For signing, the authenticated attributes must be encoded as a SET - // (tag 0x31) rather than the IMPLICIT [0] (tag 0xA0) used in SignerInfo. - attrsBytesForSign := make([]byte, len(attrsBytes)) - copy(attrsBytesForSign, attrsBytes) - attrsBytesForSign[0] = 0x31 // SET tag - - attrHash := crypto.SHA256.New() - attrHash.Write(attrsBytesForSign) - attrDigest := attrHash.Sum(nil) - - // Create signature - var sig []byte - switch key := signer.PrivateKey.(type) { +// computeSignature signs the digest with the given private key. +func computeSignature(key crypto.PrivateKey, digest []byte) ([]byte, error) { + switch k := key.(type) { case *rsa.PrivateKey: - sig, err = rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, attrDigest) + return rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, digest) case *ecdsa.PrivateKey: - sig, err = ecdsa.SignASN1(rand.Reader, key, attrDigest) - } - if err != nil { - return nil, fmt.Errorf("sign: %w", err) + return ecdsa.SignASN1(rand.Reader, k, digest) + default: + return nil, fmt.Errorf("unsupported key type: %T", key) } +} - // Build SignerInfo +// buildSignerInfoBytes builds and marshals the SignerInfo structure. +func buildSignerInfoBytes(cert *x509.Certificate, attrsBytes []byte, sig []byte, digestAlg, sigAlg pkix.AlgorithmIdentifier) ([]byte, error) { issuerRaw := asn1.RawValue{FullBytes: cert.RawIssuer} - // Build SignedAttrs as IMPLICIT [0] tagged value - // attrsBytes is a SEQUENCE containing the attribute elements. - // We need to extract just the inner content bytes. innerAttrsBytes, err := extractInnerBytes(attrsBytes) if err != nil { return nil, fmt.Errorf("extract inner attrs: %w", err) @@ -153,8 +119,50 @@ func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, er Signature: sig, } - // Marshal signer info - siBytes, err := asn1.Marshal(si) + return asn1.Marshal(si) +} + +// createCMSSignature creates a CMS/PKCS#7 SignedData structure. +func createCMSSignature(hash []byte, signer Signer, cfg *signConfig) ([]byte, error) { + cert := signer.Certificate + + sigAlg, err := signatureAlgorithm(signer.PrivateKey) + if err != nil { + return nil, err + } + + digestAlg := pkix.AlgorithmIdentifier{Algorithm: oidSHA256} + + signTime := cfg.signTime + if signTime.IsZero() { + signTime = time.Now() + } + + attrs, err := buildSignedAttrs(hash, signTime) + if err != nil { + return nil, fmt.Errorf("build signed attrs: %w", err) + } + + attrsBytes, err := marshalAttributes(attrs) + if err != nil { + return nil, fmt.Errorf("marshal attrs: %w", err) + } + + // For signing, authenticated attributes must be encoded as SET (0x31) + attrsBytesForSign := make([]byte, len(attrsBytes)) + copy(attrsBytesForSign, attrsBytes) + attrsBytesForSign[0] = 0x31 + + attrHash := crypto.SHA256.New() + attrHash.Write(attrsBytesForSign) + attrDigest := attrHash.Sum(nil) + + sig, err := computeSignature(signer.PrivateKey, attrDigest) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + + siBytes, err := buildSignerInfoBytes(cert, attrsBytes, sig, digestAlg, sigAlg) if err != nil { return nil, fmt.Errorf("marshal signer info: %w", err) } diff --git a/signature/verify.go b/signature/verify.go index 9094f0d..00a6930 100644 --- a/signature/verify.go +++ b/signature/verify.go @@ -314,7 +314,7 @@ func extractContentsHex(s string) ([]byte, error) { re := regexp.MustCompile(`/Contents\s*<([0-9A-Fa-f]+)>`) m := re.FindStringSubmatch(s) if m == nil { - return nil, fmt.Errorf("Contents hex string not found") + return nil, fmt.Errorf("contents hex string not found") } hexStr := strings.TrimRight(m[1], "0") // remove trailing zero padding if len(hexStr)%2 != 0 { From b590434d1d88bc66a312e0151e4f08d2f85cc187 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:00:46 +0000 Subject: [PATCH 7/7] update coverage badge to 85.0% --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa79a2e..71e702d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/gpdf-dev/gpdf.svg)](https://pkg.go.dev/github.com/gpdf-dev/gpdf) [![CI](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml/badge.svg)](https://github.com/gpdf-dev/gpdf/actions/workflows/check-code.yml) -![coverage](https://img.shields.io/badge/coverage-85.3%25-green) +![coverage](https://img.shields.io/badge/coverage-85.0%25-green) [![Go Report Card](https://goreportcard.com/badge/github.com/gpdf-dev/gpdf)](https://goreportcard.com/report/github.com/gpdf-dev/gpdf) [![Go Version](https://img.shields.io/badge/Go-%3E%3D1.22-blue)](https://go.dev/) [![Website](https://img.shields.io/badge/Website-gpdf.dev-blue)](https://gpdf.dev/)