diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 2dfcd20..579bac4 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -30,12 +30,6 @@ import ( // cffDict 使用float64存储所有数值,以统一处理整数和实数 type cffDict map[int][]float64 -// type2Operand Type2操作数 -type type2Operand struct { - value int - outStart int -} - // wrapCFFToOTF 将CFF裸数据包装为OpenType字体格式 // 入参: cffData CFF字体数据 // 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息 @@ -46,9 +40,10 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { } mapping := getCmapFromCFF(cffData, int(numGlyphs)) sanitized, err := sanitizeCFF(cffData) - if err == nil { - cffData = sanitized + if err != nil { + return nil, nil, err } + cffData = sanitized cffData, err = normalizeCFFCharstrings(cffData) if err != nil { return nil, nil, err @@ -114,8 +109,10 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) { if charStart < topStart+topSize || charSize == 0 || charStart+charSize > len(data) { return nil, fmt.Errorf("invalid CFF charstrings") } - chars := readCFFIndexItems(data, charStart) - changed := false + chars, changed, err := normalizeType2Programs(data) + if err != nil { + return nil, err + } var explicitCharset []byte if charset := dict[15]; len(charset) == 1 && (charset[0] == 1 || charset[0] == 2) { sids, _, _, _, ok := getCFFCharsetInfo(data, len(chars)) @@ -128,22 +125,6 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) { } changed = true } - for index, charstring := range chars { - normalized, inserted, err := explicitType2CounterStems(charstring) - if err != nil { - return nil, err - } - changed = changed || inserted - if bytes.Contains(normalized, []byte{12, 0}) { - var removed bool - normalized, removed, err = stripType2Dotsection(normalized) - if err != nil { - return nil, err - } - changed = changed || removed - } - chars[index] = normalized - } if !changed { return data, nil } @@ -197,110 +178,6 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) { return result, nil } -// explicitType2CounterStems 将计数掩码前省略的vstemhm补为显式指令,不改变笔画提示及掩码 -// 入参: data Type2字形程序 -// 返回: []byte 等价程序, bool 是否发生变更, error 字节码错误 -func explicitType2CounterStems(data []byte) ([]byte, bool, error) { - operands, hints := 0, 0 - for index := 0; index < len(data); { - start, op := index, data[index] - index++ - switch { - case op == 28: - index += 2 - operands++ - case op == 255: - index += 4 - operands++ - case op >= 247: - index++ - operands++ - case op >= 32: - operands++ - case op == 1 || op == 3 || op == 18 || op == 23: - hints += operands / 2 - operands = 0 - case op == 19 || op == 20: - hints += operands / 2 - index += (hints + 7) / 8 - if index > len(data) { - return nil, false, fmt.Errorf("incomplete Type2 hint mask") - } - if op == 20 && operands >= 2 { - result := make([]byte, 0, len(data)+1) - result = append(result, data[:start]...) - result = append(result, 23) - return append(result, data[start:]...), true, nil - } - operands = 0 - case op == 12 && index < len(data) && data[index] == 0: - index++ - default: - return data, false, nil - } - if index > len(data) { - return nil, false, fmt.Errorf("incomplete Type2 operand") - } - } - return data, false, nil -} - -// stripType2Dotsection 保留字形指令与掩码并移除无效果的旧指令 -// 入参: data Type2字形程序 -// 返回: []byte 标准化后的程序, bool 是否发生变更, error 错误信息 -func stripType2Dotsection(data []byte) ([]byte, bool, error) { - var result []byte - stack, hints := 0, 0 - for index := 0; index < len(data); { - start := index - operator := int(data[index]) - index++ - switch { - case operator == 28: - index += 2 - stack++ - case operator == 255: - index += 4 - stack++ - case operator >= 247 && operator <= 254: - index++ - stack++ - case operator >= 32: - stack++ - case operator == 12: - if index >= len(data) { - return nil, false, fmt.Errorf("incomplete Type2 operator") - } - operator = 1200 + int(data[index]) - index++ - if operator == 1200 { - if result == nil { - result = append(make([]byte, 0, len(data)-2), data[:start]...) - } - continue - } - default: - if operator == 1 || operator == 3 || operator == 18 || operator == 23 || operator == 19 || operator == 20 { - hints += stack / 2 - } - if operator == 19 || operator == 20 { - index += (hints + 7) / 8 - } - stack = 0 - } - if index > len(data) { - return nil, false, fmt.Errorf("incomplete Type2 charstring") - } - if result != nil { - result = append(result, data[start:index]...) - } - } - if result == nil { - return data, false, nil - } - return result, true, nil -} - // sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix // 入参: data 原始CFF数据 // 返回: []byte 清洗后的CFF数据, error 错误信息 @@ -451,23 +328,55 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict return nil, fmt.Errorf("missing charstrings") } charStringsOff := int(charStringsOffs[0]) - charStrings := readCFFIndexItems(data, charStringsOff) + charStrings, _, err := readType2Index(data, charStringsOff) + if err != nil { + return nil, err + } if len(charStrings) == 0 { return nil, fmt.Errorf("missing charstrings") } - fdSelect := make([]int, len(charStrings)) - if fdSelectVals, ok := topDict[1237]; ok && len(fdSelectVals) > 0 { - fdSelect = parseCFFFDSelect(data, int(fdSelectVals[0]), len(charStrings)) + fdSelectVals := topDict[1237] + if len(fdSelectVals) != 1 || !finite(fdSelectVals[0]) || fdSelectVals[0] < 0 || fdSelectVals[0] >= float64(len(data)) || fdSelectVals[0] != math.Trunc(fdSelectVals[0]) { + return nil, fmt.Errorf("invalid CFF FDSelect offset") } - globalSubrs := readCFFIndexItems(globalSubrIndexData, 0) - localSubrs, privateDicts := readCFFLocalSubrs(data, fdArrOff, fdCount) - inlined := make([][]byte, len(charStrings)) - for gid, cs := range charStrings { - fd := 0 - if gid < len(fdSelect) && fdSelect[gid] >= 0 && fdSelect[gid] < len(localSubrs) { - fd = fdSelect[gid] + fdSelect, err := readType2FDSelect(data, int(fdSelectVals[0]), len(charStrings), fdCount) + if err != nil { + return nil, err + } + globalSubrs, _, err := readType2Index(globalSubrIndexData, 0) + if err != nil { + return nil, err + } + fdItems, _, err := readType2Index(data, fdArrOff) + if err != nil { + return nil, err + } + if len(fdItems) != fdCount { + return nil, fmt.Errorf("invalid CFF FDArray count") + } + fonts := make([]type2Font, fdCount) + privateDicts := make([]cffDict, fdCount) + for fd, item := range fdItems { + fonts[fd] = type2Font{data: data, chars: charStrings, globals: globalSubrs, seed: 1} + privateDicts[fd], err = readType2Private(data, parseCFFDict(item)[18], &fonts[fd]) + if err != nil { + return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err) } - inlined[gid] = removeType2Hints(inlineType2CharString(cs, localSubrs[fd], globalSubrs, 0)) + } + inlined := make([][]byte, len(charStrings)) + var stack [48]float64 + for gid, cs := range charStrings { + font := &fonts[fdSelect[gid]] + var output bytes.Buffer + steps := 0 + state := type2State{font: font, args: stack[:0], widthTarget: &fonts[0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output, stripHints: true} + if len(cs) == 0 { + cs = []byte{14} + } + if _, err := state.run(cs, 0); err != nil { + return nil, fmt.Errorf("CFF glyph %d: %w", gid, err) + } + inlined[gid] = output.Bytes() } delete(topDict, 1230) delete(topDict, 1236) @@ -487,7 +396,7 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict } finalPrivData := encodeCFFDict(privateDict) charStringsData := encodeCFFIndex(inlined) - return encodeSanitizedCFF(data[:hdrSize], nameIndexData, topDict, stringIndexData, globalSubrIndexData, charStringsData, finalPrivData, nil), nil + return encodeSanitizedCFF(data[:hdrSize], nameIndexData, topDict, stringIndexData, encodeCFFIndex(nil), charStringsData, finalPrivData, nil), nil } // encodeSanitizedCFF 按字典编码长度收敛偏移并组装规范化字体 @@ -939,201 +848,6 @@ func parseCFFFDSelect(data []byte, offset int, numGlyphs int) []int { return result } -// readCFFLocalSubrs 读取CID CFF的本地子程序 -// 入参: data CFF数据, fdArrOff FDArray偏移, fdCount FD数量 -// 返回: [][][]byte 本地子程序列表, []cffDict Private字典列表 -func readCFFLocalSubrs(data []byte, fdArrOff int, fdCount int) ([][][]byte, []cffDict) { - fdItems := readCFFIndexItems(data, fdArrOff) - localSubrs := make([][][]byte, fdCount) - privateDicts := make([]cffDict, fdCount) - for i := 0; i < fdCount && i < len(fdItems); i++ { - fdDict := parseCFFDict(fdItems[i]) - privVals, ok := fdDict[18] - if !ok || len(privVals) != 2 { - privateDicts[i] = make(cffDict) - continue - } - privSize := int(privVals[0]) - privOff := int(privVals[1]) - if privSize <= 0 || privOff < 0 || privOff+privSize > len(data) { - privateDicts[i] = make(cffDict) - continue - } - privData := data[privOff : privOff+privSize] - privDict := parseCFFDict(privData) - privateDicts[i] = privDict - if subrVals, ok := privDict[19]; ok && len(subrVals) > 0 { - subrOff := privOff + int(subrVals[0]) - if subrOff >= 0 && subrOff < len(data) { - localSubrs[i] = readCFFIndexItems(data, subrOff) - } - } - } - return localSubrs, privateDicts -} - -// inlineType2CharString 内联Type2 CharString子程序调用 -// 入参: data CharString数据, localSubrs 本地子程序, globalSubrs 全局子程序, depth 递归深度 -// 返回: []byte 内联后的CharString数据 -func inlineType2CharString(data []byte, localSubrs, globalSubrs [][]byte, depth int) []byte { - if depth > 8 { - return data - } - out := make([]byte, 0, len(data)) - var stack []type2Operand - hintCount := 0 - for i := 0; i < len(data); { - b := data[i] - if b == 28 && i+2 < len(data) { - start := len(out) - out = append(out, data[i:i+3]...) - stack = append(stack, type2Operand{value: int(parseShortInt(data, i)), outStart: start}) - i += 3 - continue - } - if b >= 32 && b <= 246 { - start := len(out) - out = append(out, b) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i++ - continue - } - if b >= 247 && b <= 254 && i+1 < len(data) { - start := len(out) - out = append(out, data[i:i+2]...) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i += 2 - continue - } - if b == 255 && i+4 < len(data) { - start := len(out) - out = append(out, data[i:i+5]...) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i += 5 - continue - } - if b == 10 || b == 29 { - if len(stack) > 0 { - operand := stack[len(stack)-1] - out = out[:operand.outStart] - subrs := localSubrs - if b == 29 { - subrs = globalSubrs - } - idx := operand.value + cffSubrBias(len(subrs)) - if idx >= 0 && idx < len(subrs) { - subr := inlineType2CharString(subrs[idx], localSubrs, globalSubrs, depth+1) - if len(subr) > 0 && subr[len(subr)-1] == 11 { - subr = subr[:len(subr)-1] - } - out = append(out, subr...) - } - stack = nil - } - i++ - continue - } - if b == 11 && depth > 0 { - return out - } - out = append(out, b) - i++ - op := int(b) - if b == 12 && i < len(data) { - out = append(out, data[i]) - op = 1200 + int(data[i]) - i++ - } - switch op { - case 1, 3, 18, 23: - hintCount += len(stack) / 2 - case 19, 20: - hintCount += len(stack) / 2 - maskBytes := (hintCount + 7) / 8 - if i+maskBytes > len(data) { - maskBytes = len(data) - i - } - out = append(out, data[i:i+maskBytes]...) - i += maskBytes - } - stack = nil - } - return out -} - -// removeType2Hints 移除Type2 CharString的hint指令 -// 入参: data CharString数据 -// 返回: []byte 移除hint后的CharString数据 -func removeType2Hints(data []byte) []byte { - out := make([]byte, 0, len(data)) - var stack []type2Operand - hintCount := 0 - for i := 0; i < len(data); { - b := data[i] - if b == 28 && i+2 < len(data) { - start := len(out) - out = append(out, data[i:i+3]...) - stack = append(stack, type2Operand{value: int(parseShortInt(data, i)), outStart: start}) - i += 3 - continue - } - if b >= 32 && b <= 246 { - start := len(out) - out = append(out, b) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i++ - continue - } - if b >= 247 && b <= 254 && i+1 < len(data) { - start := len(out) - out = append(out, data[i:i+2]...) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i += 2 - continue - } - if b == 255 && i+4 < len(data) { - start := len(out) - out = append(out, data[i:i+5]...) - stack = append(stack, type2Operand{value: int(parseNumberType2(data, i)), outStart: start}) - i += 5 - continue - } - op := int(b) - opLen := 1 - if b == 12 && i+1 < len(data) { - op = 1200 + int(data[i+1]) - opLen = 2 - } - switch op { - case 1, 3, 18, 23: - if len(stack) > 0 { - out = out[:stack[0].outStart] - } - hintCount += len(stack) / 2 - stack = nil - i += opLen - continue - case 19, 20: - if len(stack) > 0 { - out = out[:stack[0].outStart] - } - hintCount += len(stack) / 2 - i += opLen - maskBytes := (hintCount + 7) / 8 - if i+maskBytes > len(data) { - maskBytes = len(data) - i - } - i += maskBytes - stack = nil - continue - } - out = append(out, data[i:i+opLen]...) - i += opLen - stack = nil - } - return out -} - // cffSubrBias 获取Type2子程序偏移 // 入参: count 子程序数量 // 返回: int 偏移量 @@ -1147,136 +861,42 @@ func cffSubrBias(count int) int { return 32768 } -// parseCFFWidths 从CFF数据中解析Glyph宽度 +// parseCFFWidths 从Type2执行状态获取字宽,保留首个清栈指令的宽度语义 // 入参: data CFF数据, numGlyphs 字形数量 -// 返回: []uint16 宽度列表, error 错误信息 +// 返回: []uint16 宽度列表, error 字体或执行错误 func parseCFFWidths(data []byte, numGlyphs int) ([]uint16, error) { - if len(data) < 4 { - return nil, fmt.Errorf("data too short") + font, err := readType2Font(data) + if err != nil { + return nil, err } - hdrSize := int(data[2]) - offset := hdrSize - _, sz := getCFFIndexCount(data, offset) - offset += sz - topDictData, sz := getCFFIndexData(data, offset) - offset += sz - if topDictData == nil { - return nil, fmt.Errorf("failed to read top dict") + if len(font.chars) != numGlyphs { + return nil, fmt.Errorf("CFF glyph count differs from metrics") } - topDict := parseCFFDict(topDictData) - _, sz = getCFFIndexCount(data, offset) - offset += sz - _, sz = getCFFIndexCount(data, offset) - offset += sz - var nominalWidthX float64 = 0 - var defaultWidthX float64 = 0 - if vals, ok := topDict[18]; ok && len(vals) == 2 { - privSize := int(vals[0]) - privOff := int(vals[1]) - if privSize > 0 && privOff+privSize <= len(data) { - privData := data[privOff : privOff+privSize] - privDict := parseCFFDict(privData) - if v, ok := privDict[20]; ok && len(v) > 0 { - defaultWidthX = v[0] - } - if v, ok := privDict[21]; ok && len(v) > 0 { - nominalWidthX = v[0] + widths := make([]uint16, numGlyphs) + var stack [48]float64 + for gid, program := range font.chars { + if len(program) == 0 { + program = []byte{14} + } + steps := 0 + state := type2State{font: font, args: stack[:0], width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps} + if _, err := state.run(program, 0); err != nil && !state.widthSet { + state.width = font.def + } + if gid == 0 && state.width < 0 { + steps = 0 + empty := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed, steps: &steps} + _, err := empty.run(program, 0) + if err == nil && !empty.pathStarted && !empty.changed { + state.width = 0 } } + if !finite(state.width) || state.width < 0 || state.width > math.MaxUint16 { + return nil, fmt.Errorf("CFF glyph %d width cannot be represented", gid) + } + widths[gid] = uint16(math.Round(state.width)) } - if vals, ok := topDict[17]; ok && len(vals) > 0 { - charStrOff := int(vals[0]) - count, _ := getCFFIndexCount(data, charStrOff) - limit := count - if numGlyphs < limit { - limit = numGlyphs - } - widths := make([]uint16, numGlyphs) - for i := range widths { - widths[i] = uint16(defaultWidthX) - } - for i := 0; i < limit; i++ { - offSize := int(data[charStrOff+2]) - p1 := charStrOff + 3 + i*offSize - p2 := p1 + offSize - off1 := readCFFOffset(data, p1, offSize) - off2 := readCFFOffset(data, p2, offSize) - dataStartBase := charStrOff + 3 + (count+1)*offSize - start := dataStartBase + (off1 - 1) - length := off2 - off1 - if start < 0 || start+length > len(data) { - widths[i] = uint16(defaultWidthX) - continue - } - csData := data[start : start+length] - w := scanCharStringWidth(csData, nominalWidthX, defaultWidthX) - widths[i] = uint16(w) - } - return widths, nil - } - return nil, fmt.Errorf("no charstrings") -} - -// scanCharStringWidth 扫描CharString获取宽度 -// 入参: data CharString数据, nominal 名义宽度, def 默认宽度 -// 返回: float64 宽度值 -func scanCharStringWidth(data []byte, nominal, def float64) float64 { - stackDepth := 0 - i := 0 - firstVal := 0.0 - for i < len(data) { - b := data[i] - if b <= 31 { - if b == 28 { - i += 3 - stackDepth++ - if stackDepth == 1 { - firstVal = parseShortInt(data, i-3) - } - } else if b == 29 { - i += 5 - stackDepth++ - } else if b == 12 { - i += 2 - if stackDepth%2 != 0 { - return nominal + firstVal - } - return def - } else if b == 19 || b == 20 { - if stackDepth%2 != 0 { - return nominal + firstVal - } - return def - } else { - if stackDepth%2 != 0 { - return nominal + firstVal - } - return def - } - } else { - stackDepth++ - if stackDepth == 1 { - firstVal = parseNumberType2(data, i) - } - if b >= 32 && b <= 246 { - i++ - } else if b >= 247 && b <= 250 { - i += 2 - } else if b >= 251 && b <= 254 { - i += 2 - } else if b == 255 { - i += 5 - } - } - } - return def -} - -// parseShortInt 解析短整数 (Type 2 CharString) -// 入参: data 数据, idx 索引 -// 返回: float64 浮点值 -func parseShortInt(data []byte, idx int) float64 { - return float64(int16(binary.BigEndian.Uint16(data[idx+1:]))) + return widths, nil } // parseNumberType2 解析Number (Type 2) diff --git a/ofdgo_font_ttf.go b/ofdgo_font_ttf.go index a7bc219..4a026cd 100644 --- a/ofdgo_font_ttf.go +++ b/ofdgo_font_ttf.go @@ -40,10 +40,10 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin tag := string(data[pos : pos+4]) offset := binary.BigEndian.Uint32(data[pos+8 : pos+12]) length := binary.BigEndian.Uint32(data[pos+12 : pos+16]) - if uint32(len(data)) >= offset+length { - existingTables[tag] = data[offset : offset+length] + if uint64(offset)+uint64(length) <= uint64(len(data)) { + existingTables[tag] = data[int(offset) : int(offset)+int(length)] padding := (4 - (length & 3)) & 3 - if offset%4 != 0 || uint32(len(data))-offset-length < padding { + if offset%4 != 0 || len(data)-int(offset)-int(length) < int(padding) { malformedDirectory = true } } else { @@ -53,6 +53,18 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin } if existingTables["CFF "] != nil { isCFFSfnt = true + sanitized, err := sanitizeCFF(existingTables["CFF "]) + if err != nil { + return false, data, nil, false, err + } + normalized, err := normalizeCFFCharstrings(sanitized) + if err != nil { + return false, data, nil, false, err + } + if !bytes.Equal(normalized, existingTables["CFF "]) { + existingTables["CFF "] = normalized + malformedDirectory = true + } } cmap, err := normalizeCmap(existingTables["cmap"]) if err != nil { diff --git a/ofdgo_font_type2.go b/ofdgo_font_type2.go new file mode 100644 index 0000000..3ef9e37 --- /dev/null +++ b/ofdgo_font_type2.go @@ -0,0 +1,502 @@ +// 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" + "math" + "slices" +) + +// type2Font 保存Type2字形程序、子程序及私有字宽 +type type2Font struct { + data []byte + chars [][]byte + locals [][]byte + globals [][]byte + names map[string]int + nominal, def float64 + seed uint64 +} + +// type2State 按Type2规范保存跨子程序共享的执行状态 +type type2State struct { + font *type2Font + widthTarget *type2Font + args []float64 + transient [32]float64 + stored uint32 + width float64 + widthSet bool + widthOnly bool + hints int + x, y float64 + outX, outY float64 + originX, originY float64 + component bool + stripHints bool + pathStarted bool + changed bool + output *bytes.Buffer + seed uint64 + steps *int +} + +// readType2Index 读取并校验CFF索引的全部偏移,不复制程序字节 +// 入参: data 字体数据, offset 索引偏移 +// 返回: [][]byte 索引项, int 索引结束位置, error 错误信息 +func readType2Index(data []byte, offset int) ([][]byte, int, error) { + if offset < 0 || offset > len(data)-2 { + return nil, 0, fmt.Errorf("truncated CFF index") + } + count := int(binary.BigEndian.Uint16(data[offset:])) + if count == 0 { + return nil, offset + 2, nil + } + if offset > len(data)-3 { + return nil, 0, fmt.Errorf("truncated CFF index header") + } + width := int(data[offset+2]) + if width < 1 || width > 4 || count+1 > (len(data)-offset-3)/width { + return nil, 0, fmt.Errorf("invalid CFF index offsets") + } + base := offset + 3 + (count+1)*width + items := make([][]byte, count) + previous := uint64(0) + for index := 0; index <= count; index++ { + value := uint64(0) + for _, b := range data[offset+3+index*width : offset+3+(index+1)*width] { + value = value<<8 | uint64(b) + } + if value < 1 || value-1 > uint64(len(data)-base) || value < previous || index == 0 && value != 1 { + return nil, 0, fmt.Errorf("invalid CFF index range") + } + if index != 0 { + items[index-1] = data[base+int(previous)-1 : base+int(value)-1] + } + previous = value + } + return items, base + int(previous) - 1, nil +} + +// readType2Font 读取单FD字体的Type2执行资源,CID字体先经字体规范化 +// 入参: data CFF字体数据 +// 返回: *type2Font 执行资源, error 错误信息 +func readType2Font(data []byte) (*type2Font, error) { + if !isBareCFFData(data) { + return nil, fmt.Errorf("invalid CFF header") + } + names, end, err := readType2Index(data, int(data[2])) + if err != nil { + return nil, fmt.Errorf("invalid CFF name index: %w", err) + } + if len(names) != 1 { + return nil, fmt.Errorf("invalid CFF font count") + } + tops, end, err := readType2Index(data, end) + if err != nil { + return nil, fmt.Errorf("invalid CFF top index: %w", err) + } + if len(tops) != 1 { + return nil, fmt.Errorf("invalid CFF top dictionary count") + } + _, end, err = readType2Index(data, end) + if err != nil { + return nil, err + } + globals, _, err := readType2Index(data, end) + if err != nil { + return nil, err + } + dict := parseCFFDict(tops[0]) + if value, ok := dict[1206]; ok && (len(value) != 1 || value[0] != 2) { + return nil, fmt.Errorf("unsupported CFF charstring type") + } + if _, cid := dict[1230]; cid { + return nil, fmt.Errorf("CID CFF requires FD normalization") + } + offset := dict[17] + if len(offset) != 1 || !finite(offset[0]) || offset[0] < 0 || offset[0] > float64(len(data)) || offset[0] != math.Trunc(offset[0]) { + return nil, fmt.Errorf("invalid CFF charstrings offset") + } + chars, _, err := readType2Index(data, int(offset[0])) + if err != nil { + return nil, fmt.Errorf("invalid CFF charstrings: %w", err) + } + if len(chars) == 0 { + return nil, fmt.Errorf("empty CFF charstrings") + } + font := &type2Font{data: data, chars: chars, globals: globals, seed: 1} + if _, err := readType2Private(data, dict[18], font); err != nil { + return nil, err + } + return font, nil +} + +// readType2Private 读取FD私有字宽、随机种子和局部子程序 +// 入参: data 字体数据, values Private范围, font 执行资源 +// 返回: cffDict 私有字典, error 范围或字段错误 +func readType2Private(data []byte, values []float64, font *type2Font) (cffDict, error) { + private := cffDict{} + var err error + if values != nil { + if len(values) != 2 || !finite(values[0]) || !finite(values[1]) || values[0] < 0 || values[1] < 0 || values[0] > float64(len(data)) || values[1] > float64(len(data))-values[0] || values[0] != math.Trunc(values[0]) || values[1] != math.Trunc(values[1]) { + return nil, fmt.Errorf("invalid CFF private range") + } + start := int(values[1]) + private = parseCFFDict(data[start : start+int(values[0])]) + for op, target := range map[int]*float64{20: &font.def, 21: &font.nominal} { + if value, ok := private[op]; ok { + if len(value) != 1 || !finite(value[0]) { + return nil, fmt.Errorf("invalid CFF private width") + } + *target = value[0] + } + } + if value, ok := private[1219]; ok { + if len(value) != 1 || !finite(value[0]) || value[0] < math.MinInt32 || value[0] > math.MaxInt32 || value[0] != math.Trunc(value[0]) { + return nil, fmt.Errorf("invalid CFF random seed") + } + font.seed = uint64(int64(value[0])) | 1 + } + if value, ok := private[19]; ok { + if len(value) != 1 || !finite(value[0]) || value[0] < 0 || value[0] > float64(len(data)-start) || value[0] != math.Trunc(value[0]) { + return nil, fmt.Errorf("invalid CFF local subroutine offset") + } + font.locals, _, err = readType2Index(data, start+int(value[0])) + if err != nil { + return nil, err + } + } + } + return private, nil +} + +// normalizeType2Programs 展开组合字形和求值指令,普通字形保留原程序及提示 +// 无法规范化的字形保留原程序,由实际使用时的轮廓解析校验 +// 入参: data CFF字体数据 +// 返回: [][]byte 字形程序, bool 是否变更, error 错误信息 +func normalizeType2Programs(data []byte) ([][]byte, bool, error) { + font, err := readType2Font(data) + if err != nil { + return nil, false, err + } + result := slices.Clone(font.chars) + changed := false + var stack [48]float64 + for gid, program := range font.chars { + if len(program) == 0 { + continue + } + steps := 0 + state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps} + _, err := state.run(program, 0) + if err != nil { + continue + } + if !state.changed { + continue + } + var output bytes.Buffer + steps = 0 + state = type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output} + if _, err := state.run(program, 0); err != nil { + return nil, false, fmt.Errorf("CFF glyph %d: %w", gid, err) + } + result[gid] = output.Bytes() + changed = true + } + return result, changed, nil +} + +// push 按Type2规范附录B校验并追加操作数 +// 入参: value 操作数 +// 返回: error 栈或数值错误 +func (s *type2State) push(value float64) error { + if !finite(value) || len(s.args) >= 48 { + return fmt.Errorf("invalid Type2 operand stack") + } + s.args = append(s.args, value) + return nil +} + +// emit 写入已经求值的操作数及指令,分析阶段不分配输出 +// 入参: op 操作符, values 操作数 +// 返回: error 数值编码或程序长度错误 +func (s *type2State) emit(op int, values []float64) error { + if s.output == nil { + return nil + } + for _, value := range values { + if err := encodeType2Number(s.output, value); err != nil { + return err + } + } + if op >= 1200 { + s.output.WriteByte(12) + s.output.WriteByte(byte(op - 1200)) + } else { + s.output.WriteByte(byte(op)) + } + if s.output.Len() > 65535 { + return fmt.Errorf("expanded Type2 charstring exceeds 65535 bytes") + } + return nil +} + +// takeWidth 在首次清栈指令前提取可选字宽,组件不写入自身字宽 +// 入参: explicit 是否含字宽 +// 返回: error 字宽编码错误 +func (s *type2State) takeWidth(explicit bool) error { + if s.widthSet { + return nil + } + s.widthSet = true + if explicit { + s.width = s.font.nominal + s.args[0] + if !finite(s.width) { + return fmt.Errorf("invalid Type2 width") + } + if s.output != nil && !s.component && s.widthTarget == nil { + if err := encodeType2Number(s.output, s.args[0]); err != nil { + return err + } + } + copy(s.args, s.args[1:]) + s.args = s.args[:len(s.args)-1] + } + if s.output != nil && !s.component && s.widthTarget != nil && s.width != s.widthTarget.def { + return encodeType2Number(s.output, s.width-s.widthTarget.nominal) + } + return nil +} + +// run 执行Type2程序,操作数和暂存数组在子程序间共享 +// 入参: data 字形或子程序, depth 调用深度 +// 返回: bool 是否结束字形, error 指令错误 +func (s *type2State) run(data []byte, depth int) (bool, error) { + if depth > 10 || len(data) > 65535 { + return false, fmt.Errorf("Type2 implementation limit exceeded") + } + for index := 0; index < len(data); { + (*s.steps)++ + if *s.steps > 1<<20 { + return false, fmt.Errorf("Type2 execution limit exceeded") + } + op := int(data[index]) + start := index + index++ + if op == 28 || op >= 32 { + length := 1 + if op == 28 { + length = 3 + } else if op == 255 { + length = 5 + } else if op >= 247 { + length = 2 + } + if length > len(data)-start { + return false, fmt.Errorf("truncated Type2 number") + } + index = start + length + if err := s.push(parseNumberType2(data, start)); err != nil { + return false, err + } + continue + } + if op == 12 { + if index == len(data) { + return false, fmt.Errorf("truncated Type2 operator") + } + op = 1200 + int(data[index]) + index++ + } + switch op { + case 10, 29: + if len(s.args) == 0 { + return false, fmt.Errorf("missing Type2 subroutine number") + } + value := s.args[len(s.args)-1] + s.args = s.args[:len(s.args)-1] + subrs := s.font.locals + if op == 29 { + subrs = s.font.globals + } + value += float64(cffSubrBias(len(subrs))) + if value < 0 || value >= float64(len(subrs)) || value != math.Trunc(value) { + return false, fmt.Errorf("invalid Type2 subroutine number") + } + ended, err := s.run(subrs[int(value)], depth+1) + if ended || err != nil { + return ended, err + } + case 11: + if depth == 0 { + return false, fmt.Errorf("Type2 return outside subroutine") + } + return false, nil + case 1, 3, 18, 23, 19, 20: + if err := s.takeWidth(len(s.args)%2 == 1); err != nil { + return false, err + } + if s.widthOnly { + return true, nil + } + if len(s.args)%2 != 0 || s.hints+len(s.args)/2 > 96 { + return false, fmt.Errorf("invalid Type2 stem hints") + } + s.hints += len(s.args) / 2 + if op == 19 || op == 20 { + if s.hints == 0 { + return false, fmt.Errorf("Type2 mask has no stem hints") + } + length := (s.hints + 7) / 8 + if length > len(data)-index { + return false, fmt.Errorf("truncated Type2 hint mask") + } + if !s.component && !s.stripHints && len(s.args) > 0 { + if err := s.emit(23, s.args); err != nil { + return false, err + } + if op == 20 { + s.changed = true + } + } + if !s.component && !s.stripHints { + if err := s.emit(op, nil); err != nil { + return false, err + } + if s.output != nil { + s.output.Write(data[index : index+length]) + } + } + index += length + } else if !s.component && !s.stripHints { + if err := s.emit(op, s.args); err != nil { + return false, err + } + } + s.args = s.args[:0] + case 4, 21, 22: + count := 1 + if op == 21 { + count = 2 + } + if err := s.takeWidth(len(s.args) == count+1); err != nil { + return false, err + } + if len(s.args) != count { + return false, fmt.Errorf("invalid Type2 moveto operands") + } + if s.widthOnly { + return true, nil + } + if op != 4 { + s.x += s.args[0] + } + if op != 22 { + s.y += s.args[count-1] + } + if !finite(s.x) || !finite(s.y) { + return false, fmt.Errorf("invalid Type2 current point") + } + if s.component { + if err := s.emit(21, []float64{s.x + s.originX - s.outX, s.y + s.originY - s.outY}); err != nil { + return false, err + } + } else if err := s.emit(op, s.args); err != nil { + return false, err + } + s.outX, s.outY = s.x+s.originX, s.y+s.originY + s.pathStarted = true + s.args = s.args[:0] + case 14: + if err := s.takeWidth(len(s.args) == 1 || len(s.args) == 5); err != nil { + return false, err + } + if s.widthOnly { + return true, nil + } + if len(s.args) == 4 { + if s.component { + return false, fmt.Errorf("nested Type2 composite glyph") + } + if err := s.appendComponent(s.args[2], 0, 0); err != nil { + return false, err + } + if err := s.appendComponent(s.args[3], s.args[0], s.args[1]); err != nil { + return false, err + } + s.changed = true + } else if len(s.args) != 0 { + return false, fmt.Errorf("invalid Type2 endchar operands") + } + if !s.component { + if err := s.emit(14, nil); err != nil { + return false, err + } + } + return true, nil + case 1200: + s.changed = true + case 5, 6, 7, 8, 24, 25, 26, 27, 30, 31, 1234, 1235, 1236, 1237: + if !s.pathStarted { + return false, fmt.Errorf("Type2 path has no moveto") + } + if err := s.draw(op); err != nil { + return false, err + } + default: + if err := s.calculate(op); err != nil { + return false, err + } + s.changed = true + } + } + return false, fmt.Errorf("Type2 program has no terminator") +} + +// appendComponent 按StandardEncoding查找组合组件,保持源字宽与独立暂存状态 +// 入参: code 标准字符码, x 横向偏移, y 纵向偏移 +// 返回: error 组件或指令错误 +func (s *type2State) appendComponent(code, x, y float64) error { + if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" { + return fmt.Errorf("invalid Type2 component code") + } + if s.font.names == nil { + sids, _, _, index, ok := getCFFCharsetInfo(s.font.data, len(s.font.chars)) + if !ok { + return fmt.Errorf("invalid Type2 composite charset") + } + s.font.names = make(map[string]int, len(sids)) + for gid, sid := range sids { + s.font.names[getCFFSIDString(s.font.data, index, sid)] = gid + } + } + name := type1StandardNames[int(code)] + gid, ok := s.font.names[name] + if !ok { + return fmt.Errorf("missing Type2 component %s", name) + } + child := type2State{font: s.font, width: s.font.def, component: true, output: s.output, originX: x, originY: y, outX: s.outX, outY: s.outY, seed: s.font.seed + uint64(gid), steps: s.steps} + _, err := child.run(s.font.chars[gid], 0) + if err != nil { + return fmt.Errorf("invalid Type2 component %s: %w", name, err) + } + s.outX, s.outY = child.outX, child.outY + return nil +} diff --git a/ofdgo_font_type2_fd.go b/ofdgo_font_type2_fd.go new file mode 100644 index 0000000..9abf352 --- /dev/null +++ b/ofdgo_font_type2_fd.go @@ -0,0 +1,69 @@ +// 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 ( + "encoding/binary" + "fmt" +) + +// readType2FDSelect 校验CID字体的逐字形FD归属和范围终点 +// 入参: data 字体数据, offset FDSelect偏移, count 字形数, fonts FD数量 +// 返回: []int FD索引, error 格式或范围错误 +func readType2FDSelect(data []byte, offset, count, fonts int) ([]int, error) { + if offset < 0 || offset >= len(data) || count <= 0 || fonts <= 0 { + return nil, fmt.Errorf("invalid CFF FDSelect range") + } + result := make([]int, count) + switch data[offset] { + case 0: + if count > len(data)-offset-1 { + return nil, fmt.Errorf("truncated CFF FDSelect") + } + for gid, fd := range data[offset+1 : offset+1+count] { + if int(fd) >= fonts { + return nil, fmt.Errorf("invalid CFF font dictionary index") + } + result[gid] = int(fd) + } + case 3: + if offset > len(data)-3 { + return nil, fmt.Errorf("truncated CFF FDSelect header") + } + ranges := int(binary.BigEndian.Uint16(data[offset+1:])) + start := offset + 3 + if ranges == 0 || ranges > (len(data)-start-2)/3 { + return nil, fmt.Errorf("invalid CFF FDSelect ranges") + } + if binary.BigEndian.Uint16(data[start:]) != 0 || int(binary.BigEndian.Uint16(data[start+3*ranges:])) != count { + return nil, fmt.Errorf("invalid CFF FDSelect endpoints") + } + for index := 0; index < ranges; index++ { + pos := start + 3*index + first := int(binary.BigEndian.Uint16(data[pos:])) + last := int(binary.BigEndian.Uint16(data[pos+3:])) + fd := int(data[pos+2]) + if last <= first || last > count || fd >= fonts { + return nil, fmt.Errorf("invalid CFF FDSelect interval") + } + for gid := first; gid < last; gid++ { + result[gid] = fd + } + } + default: + return nil, fmt.Errorf("unsupported CFF FDSelect format %d", data[offset]) + } + return result, nil +} diff --git a/ofdgo_font_type2_stack.go b/ofdgo_font_type2_stack.go new file mode 100644 index 0000000..b2228a1 --- /dev/null +++ b/ofdgo_font_type2_stack.go @@ -0,0 +1,268 @@ +// 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 ( + "fmt" + "math" + "slices" +) + +// calculate 求值Type2算术、条件、栈及暂存指令,不写入输出程序 +// 入参: op 操作符 +// 返回: error 操作数或运算错误 +func (s *type2State) calculate(op int) error { + count := 0 + switch op { + case 1223: + s.seed = uint64(uint32(s.seed*1664525 + 1013904223)) + return s.push(float64(s.seed+1) / 4294967296) + case 1205, 1209, 1214, 1218, 1221, 1226, 1227, 1229: + count = 1 + case 1203, 1204, 1210, 1211, 1212, 1215, 1220, 1224, 1228, 1230: + count = 2 + case 1222: + count = 4 + default: + return fmt.Errorf("unsupported Type2 operator %d", op) + } + if len(s.args) < count { + return fmt.Errorf("missing Type2 operands for operator %d", op) + } + values := s.args[len(s.args)-count:] + value := values[0] + switch op { + case 1203: + value = 0 + if values[0] != 0 && values[1] != 0 { + value = 1 + } + case 1204: + value = 0 + if values[0] != 0 || values[1] != 0 { + value = 1 + } + case 1205: + value = 0 + if values[0] == 0 { + value = 1 + } + case 1209: + value = math.Abs(value) + case 1210: + value += values[1] + case 1211: + value -= values[1] + case 1212: + value /= values[1] + case 1214: + value = -value + case 1215: + value = 0 + if values[0] == values[1] { + value = 1 + } + case 1218: + s.args = s.args[:len(s.args)-1] + return nil + case 1220, 1221: + index := values[count-1] + if index < 0 || index >= float64(len(s.transient)) || index != math.Trunc(index) { + return fmt.Errorf("invalid Type2 transient index") + } + if op == 1220 { + s.transient[int(index)] = value + s.stored |= 1 << uint(index) + s.args = s.args[:len(s.args)-2] + return nil + } + if s.stored&(1< values[3] { + value = values[1] + } + case 1224: + value *= values[1] + case 1226: + value = math.Sqrt(value) + case 1227: + return s.push(value) + case 1228: + values[0], values[1] = values[1], values[0] + return nil + case 1229: + if value != math.Trunc(value) || value >= float64(len(s.args)-1) { + return fmt.Errorf("invalid Type2 stack index") + } + index := max(0, value) + if len(s.args) < 2 { + return fmt.Errorf("empty Type2 indexed stack") + } + value = s.args[len(s.args)-2-int(index)] + case 1230: + n, j := values[0], values[1] + if n < 0 || n > float64(len(s.args)-2) || n != math.Trunc(n) || j != math.Trunc(j) { + return fmt.Errorf("invalid Type2 stack roll") + } + s.args = s.args[:len(s.args)-2] + if n == 0 { + return nil + } + shift := int(math.Mod(j, n)) + if shift < 0 { + shift += int(n) + } + tail := s.args[len(s.args)-int(n):] + slices.Reverse(tail) + slices.Reverse(tail[:shift]) + slices.Reverse(tail[shift:]) + return nil + } + s.args = s.args[:len(s.args)-count] + return s.push(value) +} + +// draw 校验Type2路径指令并推进当前点,保留曲线及Flex编码 +// 入参: op 操作符 +// 返回: error 路径操作数或输出错误 +func (s *type2State) draw(op int) error { + a := s.args + valid := true + dx, dy := 0.0, 0.0 + switch op { + case 5: + valid = len(a) >= 2 && len(a)%2 == 0 + if valid { + for i := 0; i < len(a); i += 2 { + dx, dy = dx+a[i], dy+a[i+1] + } + } + case 6, 7: + valid = len(a) > 0 + for i, value := range a { + if (i%2 == 0) == (op == 6) { + dx += value + } else { + dy += value + } + } + case 8, 24, 25: + start, end := 0, len(a) + if op == 24 { + end -= 2 + valid = len(a) >= 8 && end%6 == 0 + } else if op == 25 { + start = len(a) - 6 + valid = len(a) >= 8 && start%2 == 0 + } else { + valid = len(a) >= 6 && len(a)%6 == 0 + } + if valid { + for i := start; i < end; i += 2 { + dx, dy = dx+a[i], dy+a[i+1] + } + for i := 0; i < start; i += 2 { + dx, dy = dx+a[i], dy+a[i+1] + } + if end < len(a) { + dx, dy = dx+a[end], dy+a[end+1] + } + } + case 26, 27: + valid = len(a) >= 4 && (len(a)%4 == 0 || len(a)%4 == 1) + if valid { + i := 0 + if len(a)%4 == 1 { + if op == 26 { + dx = a[0] + } else { + dy = a[0] + } + i++ + } + for ; i < len(a); i += 4 { + if op == 26 { + dx, dy = dx+a[i+1], dy+a[i]+a[i+2]+a[i+3] + } else { + dx, dy = dx+a[i]+a[i+1]+a[i+3], dy+a[i+2] + } + } + } + case 30, 31: + valid = len(a) >= 4 && (len(a)%4 == 0 || len(a)%4 == 1) + if valid { + horizontal := op == 31 + for i := 0; i+4 <= len(a); i += 4 { + if horizontal { + dx, dy = dx+a[i]+a[i+1], dy+a[i+2]+a[i+3] + } else { + dx, dy = dx+a[i+1]+a[i+3], dy+a[i]+a[i+2] + } + if i+5 == len(a) { + if horizontal { + dx += a[i+4] + } else { + dy += a[i+4] + } + } + horizontal = !horizontal + } + } + case 1234: + valid = len(a) == 7 + if valid { + dx = a[0] + a[1] + a[3] + a[4] + a[5] + a[6] + } + case 1235: + valid = len(a) == 13 + if valid { + for i := 0; i < 12; i += 2 { + dx, dy = dx+a[i], dy+a[i+1] + } + } + case 1236: + valid = len(a) == 9 + if valid { + dx = a[0] + a[2] + a[4] + a[5] + a[6] + a[8] + } + case 1237: + valid = len(a) == 11 + if valid { + for i := 0; i < 10; i += 2 { + dx, dy = dx+a[i], dy+a[i+1] + } + if math.Abs(dx) > math.Abs(dy) { + dx, dy = dx+a[10], 0 + } else { + dx, dy = 0, dy+a[10] + } + } + default: + valid = false + } + if !valid || !finite(s.x+dx) || !finite(s.y+dy) { + return fmt.Errorf("invalid Type2 path operands for operator %d", op) + } + if err := s.emit(op, a); err != nil { + return err + } + s.x, s.y = s.x+dx, s.y+dy + s.outX, s.outY = s.x+s.originX, s.y+s.originY + s.args = s.args[:0] + return nil +} diff --git a/ofdgo_image.go b/ofdgo_image.go index d751400..b5c24b5 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -91,21 +91,21 @@ func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect } // 返回: color.Model 颜色模型 func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model } -// At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色 +// NRGBA64At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色 // 入参: x 横向坐标, y 纵向坐标 -// 返回: color.Color 非预乘颜色 -func (s *transparentEdgeImage64) At(x, y int) color.Color { +// 返回: color.NRGBA64 非预乘颜色 +func (s *transparentEdgeImage64) NRGBA64At(x, y int) color.NRGBA64 { if !image.Pt(x, y).In(s.rect) { return color.NRGBA64{} } b := s.source.Bounds() sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1) - c := color.NRGBA64Model.Convert(s.source.At(sx, sy)).(color.NRGBA64) + c := imageNRGBA64At(s.source, sx, sy) if c.A == 0 { var best color.NRGBA64 for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ { for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ { - v := color.NRGBA64Model.Convert(s.source.At(nx, ny)).(color.NRGBA64) + v := imageNRGBA64At(s.source, nx, ny) if v.A > best.A { best = v } @@ -122,11 +122,16 @@ func (s *transparentEdgeImage64) At(x, y int) color.Color { return c } +// At 返回非预乘十六位像素颜色 +// 入参: x 横向坐标, y 纵向坐标 +// 返回: color.Color 像素颜色 +func (s *transparentEdgeImage64) At(x, y int) color.Color { return s.NRGBA64At(x, y) } + // RGBA64At 返回保留原采样精度的预乘颜色 // 入参: x 横向坐标, y 纵向坐标 // 返回: color.RGBA64 预乘颜色 func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 { - r, g, b, a := s.At(x, y).RGBA() + r, g, b, a := s.NRGBA64At(x, y).RGBA() return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} } @@ -426,6 +431,38 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA { return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) } +// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱 +// 入参: img 图片对象, x X坐标, y Y坐标 +// 返回: color.NRGBA64 NRGBA64像素 +func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { + switch src := img.(type) { + case *image.NRGBA64: + return src.NRGBA64At(x, y) + case *image.Gray16: + c := src.Gray16At(x, y) + return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff} + case *image.Alpha16: + c := src.Alpha16At(x, y) + if c.A == 0 { + return color.NRGBA64{} + } + return color.NRGBA64{R: 0xffff, G: 0xffff, B: 0xffff, A: c.A} + case *image.RGBA, *image.RGBA64: + c := src.(image.RGBA64Image).RGBA64At(x, y) + if c.A == 0 { + return color.NRGBA64{} + } + r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A) + if a != 0xffff { + r = r * 0xffff / a + g = g * 0xffff / a + b = b * 0xffff / a + } + return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A} + } + return color.NRGBA64Model.Convert(img.At(x, y)).(color.NRGBA64) +} + // transparentEdgeColor 获取透明像素相邻的可见颜色 // 入参: img 图片对象, x X坐标, y Y坐标 // 返回: color.NRGBA 颜色, bool 是否存在