png-stripper.go (6808B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package media 19 20 /* 21 The code in this file is taken from the following source: 22 https://github.com/google/wuffs/blob/414a011491ff513b86d8694c5d71800f3cb5a715/script/strip-png-ancillary-chunks.go 23 24 It presents a workaround for this issue: https://github.com/golang/go/issues/43382 25 26 The license for the copied code is reproduced below: 27 28 Copyright 2021 The Wuffs Authors. 29 30 Licensed under the Apache License, Version 2.0 (the "License"); 31 you may not use this file except in compliance with the License. 32 You may obtain a copy of the License at 33 34 https://www.apache.org/licenses/LICENSE-2.0 35 36 Unless required by applicable law or agreed to in writing, software 37 distributed under the License is distributed on an "AS IS" BASIS, 38 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 See the License for the specific language governing permissions and 40 limitations under the License. 41 */ 42 43 // strip-png-ancillary-chunks.go copies PNG data from stdin to stdout, removing 44 // any ancillary chunks. 45 // 46 // Specification-compliant PNG decoders are required to honor critical chunks 47 // but may ignore ancillary (non-critical) chunks. Stripping out ancillary 48 // chunks before decoding should mean that different PNG decoders will agree on 49 // the decoded output regardless of which ancillary chunk types they choose to 50 // honor. Specifically, some PNG decoders may implement color and gamma 51 // correction but not all do. 52 // 53 // This program will strip out all ancillary chunks, but it should be 54 // straightforward to copy-paste-and-modify it to strip out only certain chunk 55 // types (e.g. only "tRNS" transparency chunks). 56 // 57 // -------- 58 // 59 // A PNG file consists of an 8-byte magic identifier and then a series of 60 // chunks. Each chunk is: 61 // 62 // - a 4-byte uint32 payload length N. 63 // - a 4-byte chunk type (e.g. "gAMA" for gamma correction metadata). 64 // - an N-byte payload. 65 // - a 4-byte CRC-32 checksum of the previous (N + 4) bytes, including the 66 // chunk type but excluding the payload length. 67 // 68 // Chunk types consist of 4 ASCII letters. The upper-case / lower-case bit of 69 // the first letter denote critical or ancillary chunks: "IDAT" and "PLTE" are 70 // critical, "gAMA" and "tEXt" are ancillary. See 71 // https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-naming-conventions 72 // 73 // -------- 74 75 import ( 76 "encoding/binary" 77 "io" 78 ) 79 80 const ( 81 chunkTypeIHDR = 0x49484452 82 chunkTypePLTE = 0x504C5445 83 chunkTypeIDAT = 0x49444154 84 chunkTypeIEND = 0x49454E44 85 chunkTypeTRNS = 0x74524e53 86 ) 87 88 func isNecessaryChunkType(chunkType uint32) bool { 89 switch chunkType { 90 case chunkTypeIHDR: 91 return true 92 case chunkTypePLTE: 93 return true 94 case chunkTypeIDAT: 95 return true 96 case chunkTypeIEND: 97 return true 98 case chunkTypeTRNS: 99 return true 100 } 101 return false 102 } 103 104 // pngAncillaryChunkStripper wraps another io.Reader to strip ancillary chunks, 105 // if the data is in the PNG file format. If the data isn't PNG, it is passed 106 // through unmodified. 107 type pngAncillaryChunkStripper struct { 108 // Reader is the wrapped io.Reader. 109 Reader io.Reader 110 111 // stickyErr is the first error returned from the wrapped io.Reader. 112 stickyErr error 113 114 // buffer[rIndex:wIndex] holds data read from the wrapped io.Reader that 115 // wasn't passed through yet. 116 buffer [8]byte 117 rIndex int 118 wIndex int 119 120 // pending and discard is the number of remaining bytes for (and whether to 121 // discard or pass through) the current chunk-in-progress. 122 pending int64 123 discard bool 124 125 // notPNG is set true if the data stream doesn't start with the 8-byte PNG 126 // magic identifier. If true, the wrapped io.Reader's data (including the 127 // first up-to-8 bytes) is passed through without modification. 128 notPNG bool 129 130 // seenMagic is whether we've seen the 8-byte PNG magic identifier. 131 seenMagic bool 132 } 133 134 // Read implements io.Reader. 135 func (r *pngAncillaryChunkStripper) Read(p []byte) (int, error) { 136 for { 137 // If the wrapped io.Reader returned a non-nil error, drain r.buffer 138 // (what data we have) and return that error (if fully drained). 139 if r.stickyErr != nil { 140 n := copy(p, r.buffer[r.rIndex:r.wIndex]) 141 r.rIndex += n 142 if r.rIndex < r.wIndex { 143 return n, nil 144 } 145 return n, r.stickyErr 146 } 147 148 // Handle trivial requests, including draining our buffer. 149 if len(p) == 0 { 150 return 0, nil 151 } else if r.rIndex < r.wIndex { 152 n := copy(p, r.buffer[r.rIndex:r.wIndex]) 153 r.rIndex += n 154 return n, nil 155 } 156 157 // From here onwards, our buffer is drained: r.rIndex == r.wIndex. 158 159 // Handle non-PNG input. 160 if r.notPNG { 161 return r.Reader.Read(p) 162 } 163 164 // Continue processing any PNG chunk that's in progress, whether 165 // discarding it or passing it through. 166 for r.pending > 0 { 167 if int64(len(p)) > r.pending { 168 p = p[:r.pending] 169 } 170 n, err := r.Reader.Read(p) 171 r.pending -= int64(n) 172 r.stickyErr = err 173 if r.discard { 174 continue 175 } 176 return n, err 177 } 178 179 // We're either expecting the 8-byte PNG magic identifier or the 4-byte 180 // PNG chunk length + 4-byte PNG chunk type. Either way, read 8 bytes. 181 r.rIndex = 0 182 r.wIndex, r.stickyErr = io.ReadFull(r.Reader, r.buffer[:8]) 183 if r.stickyErr != nil { 184 // Undo io.ReadFull converting io.EOF to io.ErrUnexpectedEOF. 185 if r.stickyErr == io.ErrUnexpectedEOF { 186 r.stickyErr = io.EOF 187 } 188 continue 189 } 190 191 // Process those 8 bytes, either: 192 // - a PNG chunk (if we've already seen the PNG magic identifier), 193 // - the PNG magic identifier itself (if the input is a PNG) or 194 // - something else (if it's not a PNG). 195 //nolint:gocritic 196 if r.seenMagic { 197 // The number of pending bytes is equal to (N + 4) because of the 4 198 // byte trailer, a checksum. 199 r.pending = int64(binary.BigEndian.Uint32(r.buffer[:4])) + 4 200 chunkType := binary.BigEndian.Uint32(r.buffer[4:]) 201 r.discard = !isNecessaryChunkType(chunkType) 202 if r.discard { 203 r.rIndex = r.wIndex 204 } 205 } else if string(r.buffer[:8]) == "\x89PNG\x0D\x0A\x1A\x0A" { 206 r.seenMagic = true 207 } else { 208 r.notPNG = true 209 } 210 } 211 }