From 388b01dadec6f49a8844364a4eff7e9dffdc3ef5 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 4 Jul 2026 00:35:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BC=BA=E5=8C=96=E5=85=BC=E5=AE=B9):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BD=8D=E5=9B=BE=E5=92=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font.go | 1 + ofdgo_font_ttf.go | 13 ++-- ofdgo_font_util.go | 159 +++++++++++++++++++++++++++++++++++++++++++++ ofdgo_image.go | 147 +++++++++++++++++++++++++++++++++++++++++ ofdgo_renderer.go | 5 +- ofdgo_sign.go | 7 +- 6 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 ofdgo_image.go diff --git a/ofdgo_font.go b/ofdgo_font.go index 8e8d861..5bf2cd8 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -20,6 +20,7 @@ import ( // FixFontDataAggressive 激进修复字体数据 // 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据 +// 对显式字形映射补充私有字符cmap,便于按glyph id渲染 // 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name // 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息 func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) { diff --git a/ofdgo_font_ttf.go b/ofdgo_font_ttf.go index 737ac05..c4285e1 100644 --- a/ofdgo_font_ttf.go +++ b/ofdgo_font_ttf.go @@ -120,7 +120,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin if !missingPost && hasBadPostTable(existingTables["post"], numGlyphs, !isCFFSfnt) { missingPost = true } - if !missingHead && !missingMaxp && !missingHhea && !missingHmtx && + if !fixCmap && !missingHead && !missingMaxp && !missingHhea && !missingHmtx && !missingOS2 && !missingCmap && !missingName && !missingPost && !malformedDirectory { return false, data, nil, false, nil } @@ -166,16 +166,17 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin newTables["OS/2"] = buildOS2TableWithMetrics(ascender, descender) } var mapping map[rune]uint16 - if missingCmap && fixCmap { - if isCFFSfnt { + if fixCmap { + if !missingCmap { + mapping = parseCmapMappings(newTables["cmap"]) + } + if len(mapping) == 0 && isCFFSfnt { mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs)) } if len(mapping) == 0 { mapping = make(map[rune]uint16) - for i := uint16(0); i < numGlyphs; i++ { - mapping[packedGlyphRune(i)] = i - } } + addPackedGlyphMapping(mapping, numGlyphs) newTables["cmap"] = buildCmapTable(numGlyphs, mapping) } if missingName && fixName { diff --git a/ofdgo_font_util.go b/ofdgo_font_util.go index e2fc428..d789a80 100644 --- a/ofdgo_font_util.go +++ b/ofdgo_font_util.go @@ -76,6 +76,165 @@ func checkMissingCmap(data []byte) bool { return true } +// parseCmapMappings 解析 cmap 字符映射 +// 入参: data cmap表数据 +// 返回: map[rune]uint16 字符到字形映射 +func parseCmapMappings(data []byte) map[rune]uint16 { + if len(data) < 4 { + return nil + } + numTables := int(binary.BigEndian.Uint16(data[2:4])) + result := make(map[rune]uint16) + for i := 0; i < numTables; i++ { + pos := 4 + i*8 + if pos+8 > len(data) { + break + } + platformID := binary.BigEndian.Uint16(data[pos : pos+2]) + if platformID != 0 && platformID != 3 { + continue + } + offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8])) + if offset+2 > len(data) { + continue + } + switch binary.BigEndian.Uint16(data[offset : offset+2]) { + case 0: + parseCmapFormat0(data[offset:], result) + case 4: + parseCmapFormat4(data[offset:], result) + case 6: + parseCmapFormat6(data[offset:], result) + case 12: + parseCmapFormat12(data[offset:], result) + } + } + if len(result) == 0 { + return nil + } + return result +} + +// parseCmapFormat0 解析 cmap format 0 +// 入参: data 子表数据, result 字符映射 +func parseCmapFormat0(data []byte, result map[rune]uint16) { + if len(data) < 262 { + return + } + for i := 0; i < 256; i++ { + gid := uint16(data[6+i]) + if gid != 0 { + result[rune(i)] = gid + } + } +} + +// parseCmapFormat4 解析 cmap format 4 +// 入参: data 子表数据, result 字符映射 +func parseCmapFormat4(data []byte, result map[rune]uint16) { + if len(data) < 16 { + return + } + length := int(binary.BigEndian.Uint16(data[2:4])) + if length > len(data) { + length = len(data) + } + segCount := int(binary.BigEndian.Uint16(data[6:8]) / 2) + endPos := 14 + startPos := endPos + segCount*2 + 2 + deltaPos := startPos + segCount*2 + rangePos := deltaPos + segCount*2 + if rangePos+segCount*2 > length { + return + } + for i := 0; i < segCount; i++ { + end := binary.BigEndian.Uint16(data[endPos+i*2:]) + start := binary.BigEndian.Uint16(data[startPos+i*2:]) + delta := int16(binary.BigEndian.Uint16(data[deltaPos+i*2:])) + rangeOffsetPos := rangePos + i*2 + rangeOffset := binary.BigEndian.Uint16(data[rangeOffsetPos:]) + if start == 0xFFFF && end == 0xFFFF { + continue + } + for c := start; c <= end; c++ { + var gid uint16 + if rangeOffset == 0 { + gid = uint16(int(c) + int(delta)) + } else { + gidPos := rangeOffsetPos + int(rangeOffset) + int(c-start)*2 + if gidPos+2 > length { + continue + } + gid = binary.BigEndian.Uint16(data[gidPos:]) + if gid != 0 { + gid = uint16(int(gid) + int(delta)) + } + } + if gid != 0 { + result[rune(c)] = gid + } + if c == 0xFFFF { + break + } + } + } +} + +// parseCmapFormat6 解析 cmap format 6 +// 入参: data 子表数据, result 字符映射 +func parseCmapFormat6(data []byte, result map[rune]uint16) { + if len(data) < 10 { + return + } + firstCode := int(binary.BigEndian.Uint16(data[6:8])) + entryCount := int(binary.BigEndian.Uint16(data[8:10])) + for i := 0; i < entryCount && 10+i*2+2 <= len(data); i++ { + gid := binary.BigEndian.Uint16(data[10+i*2:]) + if gid != 0 { + result[rune(firstCode+i)] = gid + } + } +} + +// parseCmapFormat12 解析 cmap format 12 +// 入参: data 子表数据, result 字符映射 +func parseCmapFormat12(data []byte, result map[rune]uint16) { + if len(data) < 16 { + return + } + length := int(binary.BigEndian.Uint32(data[4:8])) + if length > len(data) { + length = len(data) + } + nGroups := int(binary.BigEndian.Uint32(data[12:16])) + for i := 0; i < nGroups; i++ { + pos := 16 + i*12 + if pos+12 > length { + break + } + startChar := binary.BigEndian.Uint32(data[pos:]) + endChar := binary.BigEndian.Uint32(data[pos+4:]) + startGID := binary.BigEndian.Uint32(data[pos+8:]) + for c := startChar; c <= endChar; c++ { + gid := startGID + c - startChar + if gid != 0 && gid <= 0xFFFF { + result[rune(c)] = uint16(gid) + } + if c == endChar { + break + } + } + } +} + +// addPackedGlyphMapping 添加字形ID私有映射 +// 入参: mapping 字符映射, numGlyphs 字形数量 +func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) { + for i := uint16(0); i < numGlyphs; i++ { + mapping[packedGlyphRune(i)] = i + } +} + // buildHeadTable 构建 head 表 // 入参: unitsPerEm 每em单位数 // 返回: []byte head表数据 diff --git a/ofdgo_image.go b/ofdgo_image.go new file mode 100644 index 0000000..f28f68f --- /dev/null +++ b/ofdgo_image.go @@ -0,0 +1,147 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "encoding/binary" + "fmt" + "image" + "image/color" +) + +// decodeImageData 解码图片数据 +// 入参: data 图片数据 +// 返回: image.Image 图片对象, string 图片格式, error 错误信息 +func decodeImageData(data []byte) (image.Image, string, error) { + img, format, err := image.Decode(bytes.NewReader(data)) + if err == nil { + return img, normalizeSealType(format), nil + } + if img, err := decodeBMPImage(data); err == nil { + return img, "bmp", nil + } + return nil, "", err +} + +// decodeImageConfigData 解码图片尺寸 +// 入参: data 图片数据 +// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息 +func decodeImageConfigData(data []byte) (image.Config, string, error) { + cfg, format, err := image.DecodeConfig(bytes.NewReader(data)) + if err == nil { + return cfg, normalizeSealType(format), nil + } + if cfg, err := decodeBMPConfig(data); err == nil { + return cfg, "bmp", nil + } + return image.Config{}, "", err +} + +// decodeBMPConfig 解码 BMP 尺寸 +// 入参: data 图片数据 +// 返回: image.Config 图片尺寸, error 错误信息 +func decodeBMPConfig(data []byte) (image.Config, error) { + offset, width, height, bpp, compression, err := parseBMPHeader(data) + if err != nil { + return image.Config{}, err + } + if offset <= 0 || width <= 0 || height == 0 || compression != 0 { + return image.Config{}, fmt.Errorf("unsupported bmp") + } + if bpp != 16 && bpp != 24 && bpp != 32 { + return image.Config{}, fmt.Errorf("unsupported bmp") + } + if height < 0 { + height = -height + } + return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil +} + +// decodeBMPImage 解码 BMP 图片 +// 入参: data 图片数据 +// 返回: image.Image 图片对象, error 错误信息 +func decodeBMPImage(data []byte) (image.Image, error) { + offset, width, height, bpp, compression, err := parseBMPHeader(data) + if err != nil { + return nil, err + } + if compression != 0 { + return nil, fmt.Errorf("unsupported bmp compression") + } + topDown := height < 0 + if topDown { + height = -height + } + if width <= 0 || height <= 0 { + return nil, fmt.Errorf("invalid bmp size") + } + rowStride := ((width*int(bpp) + 31) / 32) * 4 + if offset+rowStride*height > len(data) { + return nil, fmt.Errorf("truncated bmp") + } + img := image.NewNRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + srcY := y + if !topDown { + srcY = height - 1 - y + } + row := data[offset+srcY*rowStride:] + for x := 0; x < width; x++ { + switch bpp { + case 16: + v := binary.LittleEndian.Uint16(row[x*2:]) + img.SetNRGBA(x, y, color.NRGBA{ + R: uint8(((v >> 10) & 0x1F) * 255 / 31), + G: uint8(((v >> 5) & 0x1F) * 255 / 31), + B: uint8((v & 0x1F) * 255 / 31), + A: 255, + }) + case 24: + p := x * 3 + img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255}) + case 32: + p := x * 4 + img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255}) + default: + return nil, fmt.Errorf("unsupported bmp") + } + } + } + return img, nil +} + +// parseBMPHeader 解析 BMP 文件头 +// 入参: data 图片数据 +// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息 +func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) { + if len(data) < 54 || string(data[:2]) != "BM" { + return 0, 0, 0, 0, 0, fmt.Errorf("not bmp") + } + offset := int(binary.LittleEndian.Uint32(data[10:14])) + dibSize := binary.LittleEndian.Uint32(data[14:18]) + if dibSize < 40 { + return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp") + } + width := int(int32(binary.LittleEndian.Uint32(data[18:22]))) + height := int(int32(binary.LittleEndian.Uint32(data[22:26]))) + planes := binary.LittleEndian.Uint16(data[26:28]) + bpp := binary.LittleEndian.Uint16(data[28:30]) + compression := binary.LittleEndian.Uint32(data[30:34]) + if planes != 1 { + return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp") + } + return offset, width, height, bpp, compression, nil +} diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 9801959..f43598d 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -32,6 +32,7 @@ import ( "github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/rasterizer" _ "github.com/xiaoqidun/jbig2" + _ "golang.org/x/image/bmp" ) // Renderer 渲染器实现 @@ -501,7 +502,7 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float if err != nil { return } - img, _, err := image.Decode(bytes.NewReader(imgData)) + img, _, err := decodeImageData(imgData) if err != nil { return } @@ -1420,7 +1421,7 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { } } if len(s.Data) > 0 { - img, _, err := image.Decode(bytes.NewReader(s.Data)) + img, _, err := decodeImageData(s.Data) if err == nil { img = stampImageWithTransparentWhite(img) ctx.Push() diff --git a/ofdgo_sign.go b/ofdgo_sign.go index 8eff071..f69b3fa 100644 --- a/ofdgo_sign.go +++ b/ofdgo_sign.go @@ -19,7 +19,6 @@ import ( "encoding/asn1" "encoding/binary" "encoding/xml" - "image" "strings" ) @@ -220,6 +219,8 @@ func normalizeSealType(s string) string { switch s { case "jpg": return "jpeg" + case "bmp": + return "bmp" case "jb2", "gbig2": return "jbig2" default: @@ -232,7 +233,7 @@ func normalizeSealType(s string) string { // 返回: bool 是否可渲染 func isSealMediaType(s string) bool { switch s { - case "png", "jpeg", "jbig2", "ofd": + case "png", "jpeg", "bmp", "jbig2", "ofd": return true default: return false @@ -286,7 +287,7 @@ func zipDataEnd(data []byte) int { // 入参: data 原始数据 // 返回: string 媒体类型, []byte 图片数据 func probeImageMedia(data []byte) (string, []byte) { - _, format, err := image.DecodeConfig(bytes.NewReader(data)) + _, format, err := decodeImageConfigData(data) if err != nil { return "", nil }