gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }