mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:02:25 -05:00 
			
		
		
		
	[chore] bump exif-terminator to 0.4.0 (#747)
This commit is contained in:
		
					parent
					
						
							
								91c8d5d20d
							
						
					
				
			
			
				commit
				
					
						2462c5fe22
					
				
			
		
					 5 changed files with 90 additions and 45 deletions
				
			
		
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -39,7 +39,7 @@ require ( | ||||||
| 	github.com/spf13/viper v1.11.0 | 	github.com/spf13/viper v1.11.0 | ||||||
| 	github.com/stretchr/testify v1.7.1 | 	github.com/stretchr/testify v1.7.1 | ||||||
| 	github.com/superseriousbusiness/activity v1.1.0-gts | 	github.com/superseriousbusiness/activity v1.1.0-gts | ||||||
| 	github.com/superseriousbusiness/exif-terminator v0.3.0 | 	github.com/superseriousbusiness/exif-terminator v0.4.0 | ||||||
| 	github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB | 	github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB | ||||||
| 	github.com/tdewolff/minify/v2 v2.12.0 | 	github.com/tdewolff/minify/v2 v2.12.0 | ||||||
| 	github.com/uptrace/bun v1.1.3 | 	github.com/uptrace/bun v1.1.3 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -512,8 +512,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s | ||||||
| github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | ||||||
| github.com/superseriousbusiness/activity v1.1.0-gts h1:BSnMzs/84s0Zme7BngE9iJAHV7g1Bv1nhLCP0aJtU3I= | github.com/superseriousbusiness/activity v1.1.0-gts h1:BSnMzs/84s0Zme7BngE9iJAHV7g1Bv1nhLCP0aJtU3I= | ||||||
| github.com/superseriousbusiness/activity v1.1.0-gts/go.mod h1:AZw0Xb4Oju8rmaJCZ21gc5CPg47MmNgyac+Hx5jo8VM= | github.com/superseriousbusiness/activity v1.1.0-gts/go.mod h1:AZw0Xb4Oju8rmaJCZ21gc5CPg47MmNgyac+Hx5jo8VM= | ||||||
| github.com/superseriousbusiness/exif-terminator v0.3.0 h1:ej7YePEB2UnAGPal5s7CnoN8eMFmDFESEAEJmbFoHh0= | github.com/superseriousbusiness/exif-terminator v0.4.0 h1:pzAg7luCi8oc2LVDwgTLvTinh/+/2UuWgJZrM8MMaT4= | ||||||
| github.com/superseriousbusiness/exif-terminator v0.3.0/go.mod h1:OPfOSEDWjXaW3BILJBN89j0VLD8bglmHwHHwwwSLb5A= | github.com/superseriousbusiness/exif-terminator v0.4.0/go.mod h1:OPfOSEDWjXaW3BILJBN89j0VLD8bglmHwHHwwwSLb5A= | ||||||
| github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe h1:ksl2oCx/Qo8sNDc3Grb8WGKBM9nkvhCm25uvlT86azE= | github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe h1:ksl2oCx/Qo8sNDc3Grb8WGKBM9nkvhCm25uvlT86azE= | ||||||
| github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4= | github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4= | ||||||
| github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB h1:PtW2w6budTvRV2J5QAoSvThTHBuvh8t/+BXIZFAaBSc= | github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB h1:PtW2w6budTvRV2J5QAoSvThTHBuvh8t/+BXIZFAaBSc= | ||||||
|  |  | ||||||
							
								
								
									
										122
									
								
								vendor/github.com/superseriousbusiness/exif-terminator/jpeg.go
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										122
									
								
								vendor/github.com/superseriousbusiness/exif-terminator/jpeg.go
									
										
									
										generated
									
									
										vendored
									
									
								
							|  | @ -73,21 +73,46 @@ var markerLen = map[byte]int{ | ||||||
| type jpegVisitor struct { | type jpegVisitor struct { | ||||||
| 	js                *jpegstructure.JpegSplitter | 	js                *jpegstructure.JpegSplitter | ||||||
| 	writer            io.Writer | 	writer            io.Writer | ||||||
|  | 	expectedFileSize  int | ||||||
|  | 	writtenTotalBytes int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // HandleSegment satisfies the visitor interface{} of the jpegstructure library. | // HandleSegment satisfies the visitor interface{} of the jpegstructure library. | ||||||
| // | // | ||||||
| // We don't really care about any of the parameters, since all we're interested | // We don't really care about many of the parameters, since all we're interested | ||||||
| // in here is the very last segment that was scanned. | // in here is the very last segment that was scanned. | ||||||
| func (v *jpegVisitor) HandleSegment(_ byte, _ string, _ int, _ bool) error { | func (v *jpegVisitor) HandleSegment(segmentMarker byte, _ string, _ int, _ bool) error { | ||||||
| 	// all we want to do here is get the last segment that was scanned, and then manipulate it | 	// get the most recent segment scanned (ie., last in the segments list) | ||||||
| 	segmentList := v.js.Segments() | 	segmentList := v.js.Segments() | ||||||
| 	segments := segmentList.Segments() | 	segments := segmentList.Segments() | ||||||
| 	lastSegment := segments[len(segments)-1] | 	mostRecentSegment := segments[len(segments)-1] | ||||||
| 	return v.writeSegment(lastSegment) | 
 | ||||||
|  | 	// check if we've written the expected number of bytes by EOI | ||||||
|  | 	if segmentMarker == jpegstructure.MARKER_EOI { | ||||||
|  | 		// take account of the last 2 bytes taken up by the EOI | ||||||
|  | 		eoiLength := 2 | ||||||
|  | 		 | ||||||
|  | 		// this is the total file size we will | ||||||
|  | 		// have written including the EOI | ||||||
|  | 		willHaveWritten := v.writtenTotalBytes + eoiLength | ||||||
|  | 
 | ||||||
|  | 		if willHaveWritten < v.expectedFileSize { | ||||||
|  | 			// if we won't have written enough, | ||||||
|  | 			// pad the final segment before EOI | ||||||
|  | 			// so that we meet expected file size | ||||||
|  | 			missingBytes := make([]byte, v.expectedFileSize-willHaveWritten) | ||||||
|  | 			if _, err := v.writer.Write(missingBytes); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// process the segment | ||||||
|  | 	return v.writeSegment(mostRecentSegment) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
|  | 	var writtenSegmentData int | ||||||
| 	w := v.writer | 	w := v.writer | ||||||
| 
 | 
 | ||||||
| 	defer func() { | 	defer func() { | ||||||
|  | @ -98,9 +123,11 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 
 | 
 | ||||||
| 	// The scan-data will have a marker-ID of (0) because it doesn't have a marker-ID or length. | 	// The scan-data will have a marker-ID of (0) because it doesn't have a marker-ID or length. | ||||||
| 	if s.MarkerId != 0 { | 	if s.MarkerId != 0 { | ||||||
| 		if _, err := w.Write([]byte{0xff, s.MarkerId}); err != nil { | 		markerIDWritten, err := w.Write([]byte{0xff, s.MarkerId}) | ||||||
|  | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 		writtenSegmentData += markerIDWritten | ||||||
| 
 | 
 | ||||||
| 		sizeLen, found := markerLen[s.MarkerId] | 		sizeLen, found := markerLen[s.MarkerId] | ||||||
| 		if !found || sizeLen == 2 { | 		if !found || sizeLen == 2 { | ||||||
|  | @ -111,6 +138,7 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			writtenSegmentData += 2 | ||||||
| 		} else if sizeLen == 4 { | 		} else if sizeLen == 4 { | ||||||
| 			l := uint32(len(s.Data) + sizeLen) | 			l := uint32(len(s.Data) + sizeLen) | ||||||
| 
 | 
 | ||||||
|  | @ -118,6 +146,7 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			writtenSegmentData += 4 | ||||||
| 		} else if sizeLen != 0 { | 		} else if sizeLen != 0 { | ||||||
| 			return fmt.Errorf("not a supported marker-size: MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", s.MarkerId, sizeLen) | 			return fmt.Errorf("not a supported marker-size: MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", s.MarkerId, sizeLen) | ||||||
| 		} | 		} | ||||||
|  | @ -125,17 +154,23 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 
 | 
 | ||||||
| 	if !s.IsExif() { | 	if !s.IsExif() { | ||||||
| 		// if this isn't exif data just copy it over and bail | 		// if this isn't exif data just copy it over and bail | ||||||
| 		_, err := w.Write(s.Data) | 		writtenNormalData, err := w.Write(s.Data) | ||||||
|  | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		writtenSegmentData += writtenNormalData | ||||||
|  | 		v.writtenTotalBytes += writtenSegmentData | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	ifd, _, err := s.Exif() | 	ifd, _, err := s.Exif() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// amount of bytes we've written into the exif body | 	// amount of bytes we've writtenExifData into the exif body, we'll update this as we go | ||||||
| 	var written int | 	var writtenExifData int | ||||||
| 
 | 
 | ||||||
| 	if orientationEntries, err := ifd.FindTagWithName("Orientation"); err == nil && len(orientationEntries) == 1 { | 	if orientationEntries, err := ifd.FindTagWithName("Orientation"); err == nil && len(orientationEntries) == 1 { | ||||||
| 		// If we have an orientation entry, we don't want to completely obliterate the exif data. | 		// If we have an orientation entry, we don't want to completely obliterate the exif data. | ||||||
|  | @ -152,30 +187,31 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 		// | 		// | ||||||
| 		// Then we write the ifd0 entry which contains the orientation data. | 		// Then we write the ifd0 entry which contains the orientation data. | ||||||
| 		// | 		// | ||||||
| 		// After that we just fill fill fill. | 		// After that we just fill. | ||||||
| 
 | 
 | ||||||
| 		newData := &bytes.Buffer{} | 		newExifData := &bytes.Buffer{} | ||||||
|  | 		byteOrder := ifd.ByteOrder() | ||||||
| 
 | 
 | ||||||
| 		// 1. Write exif prefix. | 		// 1. Write exif prefix. | ||||||
| 		// https://www.ozhiker.com/electronics/pjmt/jpeg_info/app_segments.html | 		// https://www.ozhiker.com/electronics/pjmt/jpeg_info/app_segments.html | ||||||
| 		prefix := []byte{'E', 'x', 'i', 'f', 0, 0} | 		prefix := []byte{'E', 'x', 'i', 'f', 0, 0} | ||||||
| 		if err := binary.Write(newData, ifd.ByteOrder(), &prefix); err != nil { | 		if err := binary.Write(newExifData, byteOrder, &prefix); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += 6 | 		writtenExifData += len(prefix) | ||||||
| 
 | 
 | ||||||
| 		// 2. Write exif header, taking the existing byte order. | 		// 2. Write exif header, taking the existing byte order. | ||||||
| 		exifHeader, err := exif.BuildExifHeader(ifd.ByteOrder(), exif.ExifDefaultFirstIfdOffset) | 		exifHeader, err := exif.BuildExifHeader(byteOrder, exif.ExifDefaultFirstIfdOffset) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		hWritten, err := newData.Write(exifHeader) | 		hWritten, err := newExifData.Write(exifHeader) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += hWritten | 		writtenExifData += hWritten | ||||||
| 
 | 
 | ||||||
| 		// https://web.archive.org/web/20190624045241if_/http://www.cipa.jp:80/std/documents/e/DC-008-Translation-2019-E.pdf | 		// 3. Write in the new ifd | ||||||
| 		// | 		// | ||||||
| 		// An ifd with one orientation entry is structured like this: | 		// An ifd with one orientation entry is structured like this: | ||||||
| 		// 		2 bytes: the number of entries in the ifd	uint16(1) | 		// 		2 bytes: the number of entries in the ifd	uint16(1) | ||||||
|  | @ -191,61 +227,69 @@ func (v *jpegVisitor) writeSegment(s *jpegstructure.Segment) error { | ||||||
| 		// 			6 = Rotate 90 CW | 		// 			6 = Rotate 90 CW | ||||||
| 		// 			7 = Mirror horizontal and rotate 90 CW | 		// 			7 = Mirror horizontal and rotate 90 CW | ||||||
| 		// 			8 = Rotate 270 CW | 		// 			8 = Rotate 270 CW | ||||||
|  | 		// | ||||||
|  | 		// see https://web.archive.org/web/20190624045241if_/http://www.cipa.jp:80/std/documents/e/DC-008-Translation-2019-E.pdf - p24-25 | ||||||
| 		orientationEntry := orientationEntries[0] | 		orientationEntry := orientationEntries[0] | ||||||
| 
 | 
 | ||||||
| 		ifdCount := uint16(1) // we're only adding one entry into the ifd | 		ifdCount := uint16(1) // we're only adding one entry into the ifd | ||||||
| 		if err := binary.Write(newData, ifd.ByteOrder(), &ifdCount); err != nil { | 		if err := binary.Write(newExifData, byteOrder, &ifdCount); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += 2 | 		writtenExifData += 2 | ||||||
| 
 | 
 | ||||||
| 		tagID := orientationEntry.TagId() | 		tagID := orientationEntry.TagId() | ||||||
| 		if err := binary.Write(newData, ifd.ByteOrder(), &tagID); err != nil { | 		if err := binary.Write(newExifData, byteOrder, &tagID); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += 2 | 		writtenExifData += 2 | ||||||
| 
 | 
 | ||||||
| 		tagType := orientationEntry.TagType() | 		tagType := uint16(orientationEntry.TagType()) | ||||||
| 		if err := binary.Write(newData, ifd.ByteOrder(), &tagType); err != nil { | 		if err := binary.Write(newExifData, byteOrder, &tagType); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += 2 | 		writtenExifData += 2 | ||||||
| 
 | 
 | ||||||
| 		tagCount := orientationEntry.UnitCount() | 		tagCount := orientationEntry.UnitCount() | ||||||
| 		if err := binary.Write(newData, ifd.ByteOrder(), &tagCount); err != nil { | 		if err := binary.Write(newExifData, byteOrder, &tagCount); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += 4 | 		writtenExifData += 4 | ||||||
| 
 | 
 | ||||||
| 		valueOffset, err := orientationEntry.GetRawBytes() | 		valueOffset, err := orientationEntry.GetRawBytes() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		vWritten, err := newData.Write(valueOffset) | 		vWritten, err := newExifData.Write(valueOffset) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += vWritten | 		writtenExifData += vWritten | ||||||
| 
 | 
 | ||||||
| 		valuePad := make([]byte, 4-vWritten) | 		valuePad := make([]byte, 4-vWritten) | ||||||
| 		pWritten, err := newData.Write(valuePad) | 		pWritten, err := newExifData.Write(valuePad) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		written += pWritten | 		writtenExifData += pWritten | ||||||
| 
 | 
 | ||||||
| 		// write everything in | 		// write all the new data into the writer from the segment | ||||||
| 		if _, err := io.Copy(w, newData); err != nil { | 		writtenNewExifData, err := io.Copy(w, newExifData) | ||||||
| 			return err | 		if err != nil { | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// fill in the (remaining) exif body with blank bytes |  | ||||||
| 	blank := make([]byte, len(s.Data)-written) |  | ||||||
| 	if _, err := w.Write(blank); err != nil { |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		writtenSegmentData += int(writtenNewExifData) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// fill in any remaining exif body with blank bytes | ||||||
|  | 	blank := make([]byte, len(s.Data)-writtenExifData) | ||||||
|  | 	writtenPadding, err := w.Write(blank) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	writtenSegmentData += writtenPadding | ||||||
|  | 	v.writtenTotalBytes += writtenSegmentData | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								vendor/github.com/superseriousbusiness/exif-terminator/terminator.go
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								vendor/github.com/superseriousbusiness/exif-terminator/terminator.go
									
										
									
										generated
									
									
										vendored
									
									
								
							|  | @ -42,7 +42,7 @@ func Terminate(in io.Reader, fileSize int, mediaType string) (io.Reader, error) | ||||||
| 
 | 
 | ||||||
| 	switch mediaType { | 	switch mediaType { | ||||||
| 	case "image/jpeg", "jpeg", "jpg": | 	case "image/jpeg", "jpeg", "jpg": | ||||||
| 		err = terminateJpeg(scanner, pipeWriter) | 		err = terminateJpeg(scanner, pipeWriter, fileSize) | ||||||
| 	case "image/png", "png": | 	case "image/png", "png": | ||||||
| 		// for pngs we need to skip the header bytes, so read them in | 		// for pngs we need to skip the header bytes, so read them in | ||||||
| 		// and check we're really dealing with a png here | 		// and check we're really dealing with a png here | ||||||
|  | @ -65,10 +65,11 @@ func Terminate(in io.Reader, fileSize int, mediaType string) (io.Reader, error) | ||||||
| 	return pipeReader, err | 	return pipeReader, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func terminateJpeg(scanner *bufio.Scanner, writer io.WriteCloser) error { | func terminateJpeg(scanner *bufio.Scanner, writer io.WriteCloser, expectedFileSize int) error { | ||||||
| 	// jpeg visitor is where the spicy hack of streaming the de-exifed data is contained | 	// jpeg visitor is where the spicy hack of streaming the de-exifed data is contained | ||||||
| 	v := &jpegVisitor{ | 	v := &jpegVisitor{ | ||||||
| 		writer:           writer, | 		writer:           writer, | ||||||
|  | 		expectedFileSize: expectedFileSize, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// provide the visitor to the splitter so that it triggers on every section scan | 	// provide the visitor to the splitter so that it triggers on every section scan | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								vendor/modules.txt
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/modules.txt
									
										
									
									
										vendored
									
									
								
							|  | @ -535,7 +535,7 @@ github.com/superseriousbusiness/activity/streams/values/rfc2045 | ||||||
| github.com/superseriousbusiness/activity/streams/values/rfc5988 | github.com/superseriousbusiness/activity/streams/values/rfc5988 | ||||||
| github.com/superseriousbusiness/activity/streams/values/string | github.com/superseriousbusiness/activity/streams/values/string | ||||||
| github.com/superseriousbusiness/activity/streams/vocab | github.com/superseriousbusiness/activity/streams/vocab | ||||||
| # github.com/superseriousbusiness/exif-terminator v0.3.0 | # github.com/superseriousbusiness/exif-terminator v0.4.0 | ||||||
| ## explicit; go 1.17 | ## explicit; go 1.17 | ||||||
| github.com/superseriousbusiness/exif-terminator | github.com/superseriousbusiness/exif-terminator | ||||||
| # github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe | # github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue