mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Add Image Diff for SVG files (#14867)
* Added type sniffer. * Switched content detection from base to typesniffer. * Added GuessContentType to Blob. * Moved image info logic to client. Added support for SVG images in diff. * Restore old blocked svg behaviour. * Added missing image formats. * Execute image diff only when container is visible. * add margin to spinner * improve BIN tag on image diffs * Default to render view. * Show image diff on incomplete diff. Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
@@ -37,8 +37,20 @@ func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit,
|
||||
ctx.Data["BaseCommit"] = base
|
||||
ctx.Data["HeadCommit"] = head
|
||||
|
||||
ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
blob, err := commit.GetBlobByPath(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return blob
|
||||
}
|
||||
|
||||
setPathsCompareContext(ctx, base, head, headTarget)
|
||||
setImageCompareContext(ctx, base, head)
|
||||
setImageCompareContext(ctx)
|
||||
setCsvCompareContext(ctx)
|
||||
}
|
||||
|
||||
@@ -57,27 +69,18 @@ func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Co
|
||||
}
|
||||
|
||||
// setImageCompareContext sets context data that is required by image compare template
|
||||
func setImageCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit) {
|
||||
ctx.Data["IsImageFileInHead"] = head.IsImageFile
|
||||
ctx.Data["IsImageFileInBase"] = base.IsImageFile
|
||||
ctx.Data["ImageInfoBase"] = func(name string) *git.ImageMetaData {
|
||||
if base == nil {
|
||||
return nil
|
||||
func setImageCompareContext(ctx *context.Context) {
|
||||
ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
|
||||
if blob == nil {
|
||||
return false
|
||||
}
|
||||
result, err := base.ImageInfo(name)
|
||||
|
||||
st, err := blob.GuessContentType()
|
||||
if err != nil {
|
||||
log.Error("ImageInfo failed: %v", err)
|
||||
return nil
|
||||
log.Error("GuessContentType failed: %v", err)
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
ctx.Data["ImageInfo"] = func(name string) *git.ImageMetaData {
|
||||
result, err := head.ImageInfo(name)
|
||||
if err != nil {
|
||||
log.Error("ImageInfo failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
)
|
||||
|
||||
// ServeData download file from io.Reader
|
||||
@@ -45,28 +45,32 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
|
||||
// Google Chrome dislike commas in filenames, so let's change it to a space
|
||||
name = strings.ReplaceAll(name, ",", " ")
|
||||
|
||||
if base.IsTextFile(buf) || ctx.QueryBool("render") {
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
|
||||
if st.IsText() || ctx.QueryBool("render") {
|
||||
cs, err := charset.DetectEncoding(buf)
|
||||
if err != nil {
|
||||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
|
||||
cs = "utf-8"
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
|
||||
} else if base.IsImageFile(buf) || base.IsPDFFile(buf) {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
|
||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
if base.IsSVGImageFile(buf) {
|
||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
ctx.Resp.Header().Set("Content-Type", base.SVGMimeType)
|
||||
}
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
|
||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
if setting.MimeTypeMap.Enabled {
|
||||
fileExtension := strings.ToLower(filepath.Ext(name))
|
||||
if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
|
||||
ctx.Resp.Header().Set("Content-Type", mimetype)
|
||||
|
||||
if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
|
||||
if st.IsSvgImage() {
|
||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
|
||||
}
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
|
||||
if setting.MimeTypeMap.Enabled {
|
||||
fileExtension := strings.ToLower(filepath.Ext(name))
|
||||
if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
|
||||
ctx.Resp.Header().Set("Content-Type", mimetype)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/repofiles"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/upload"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -117,8 +118,8 @@ func editFile(ctx *context.Context, isNewFile bool) {
|
||||
buf = buf[:n]
|
||||
|
||||
// Only some file types are editable online as text.
|
||||
if !base.IsRepresentableAsText(buf) {
|
||||
ctx.NotFound("base.IsRepresentableAsText", nil)
|
||||
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
|
||||
ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -25,6 +25,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -278,16 +279,16 @@ func LFSFileGet(ctx *context.Context) {
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
ctx.Data["IsTextFile"] = base.IsTextFile(buf)
|
||||
isRepresentableAsText := base.IsRepresentableAsText(buf)
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
ctx.Data["IsTextFile"] = st.IsText()
|
||||
isRepresentableAsText := st.IsRepresentableAsText()
|
||||
|
||||
fileSize := meta.Size
|
||||
ctx.Data["FileSize"] = meta.Size
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
|
||||
switch {
|
||||
case isRepresentableAsText:
|
||||
// This will be true for SVGs.
|
||||
if base.IsImageFile(buf) {
|
||||
if st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
}
|
||||
|
||||
@@ -322,13 +323,13 @@ func LFSFileGet(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
|
||||
|
||||
case base.IsPDFFile(buf):
|
||||
case st.IsPDF():
|
||||
ctx.Data["IsPDFFile"] = true
|
||||
case base.IsVideoFile(buf):
|
||||
case st.IsVideo():
|
||||
ctx.Data["IsVideoFile"] = true
|
||||
case base.IsAudioFile(buf):
|
||||
case st.IsAudio():
|
||||
ctx.Data["IsAudioFile"] = true
|
||||
case base.IsImageFile(buf):
|
||||
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplSettingsLFSFile)
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
@@ -1021,7 +1022,8 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.ReadAll: %v", err)
|
||||
}
|
||||
if !base.IsImageFile(data) {
|
||||
st := typesniffer.DetectContentType(data)
|
||||
if !(st.IsImage() && !st.IsSvgImage()) {
|
||||
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
|
||||
}
|
||||
if err = ctxRepo.UploadAvatar(data); err != nil {
|
||||
|
@@ -29,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -265,7 +266,9 @@ func renderDirectory(ctx *context.Context, treeLink string) {
|
||||
n, _ := dataRc.Read(buf)
|
||||
buf = buf[:n]
|
||||
|
||||
isTextFile := base.IsTextFile(buf)
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
isTextFile := st.IsText()
|
||||
|
||||
ctx.Data["FileIsText"] = isTextFile
|
||||
ctx.Data["FileName"] = readmeFile.name
|
||||
fileSize := int64(0)
|
||||
@@ -302,7 +305,8 @@ func renderDirectory(ctx *context.Context, treeLink string) {
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
isTextFile = base.IsTextFile(buf)
|
||||
st = typesniffer.DetectContentType(buf)
|
||||
isTextFile = st.IsText()
|
||||
ctx.Data["IsTextFile"] = isTextFile
|
||||
|
||||
fileSize = meta.Size
|
||||
@@ -405,7 +409,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||
n, _ := dataRc.Read(buf)
|
||||
buf = buf[:n]
|
||||
|
||||
isTextFile := base.IsTextFile(buf)
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
isTextFile := st.IsText()
|
||||
|
||||
isLFSFile := false
|
||||
isDisplayingSource := ctx.Query("display") == "source"
|
||||
isDisplayingRendered := !isDisplayingSource
|
||||
@@ -441,14 +447,16 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
isTextFile = base.IsTextFile(buf)
|
||||
st = typesniffer.DetectContentType(buf)
|
||||
isTextFile = st.IsText()
|
||||
|
||||
fileSize = meta.Size
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isRepresentableAsText := base.IsRepresentableAsText(buf)
|
||||
isRepresentableAsText := st.IsRepresentableAsText()
|
||||
if !isRepresentableAsText {
|
||||
// If we can't show plain text, always try to render.
|
||||
isDisplayingSource = false
|
||||
@@ -483,8 +491,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||
|
||||
switch {
|
||||
case isRepresentableAsText:
|
||||
// This will be true for SVGs.
|
||||
if base.IsImageFile(buf) {
|
||||
if st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
}
|
||||
@@ -540,13 +547,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||
}
|
||||
}
|
||||
|
||||
case base.IsPDFFile(buf):
|
||||
case st.IsPDF():
|
||||
ctx.Data["IsPDFFile"] = true
|
||||
case base.IsVideoFile(buf):
|
||||
case st.IsVideo():
|
||||
ctx.Data["IsVideoFile"] = true
|
||||
case base.IsAudioFile(buf):
|
||||
case st.IsAudio():
|
||||
ctx.Data["IsAudioFile"] = true
|
||||
case base.IsImageFile(buf):
|
||||
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
default:
|
||||
if fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
@@ -159,7 +160,9 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.ReadAll: %v", err)
|
||||
}
|
||||
if !base.IsImageFile(data) {
|
||||
|
||||
st := typesniffer.DetectContentType(data)
|
||||
if !(st.IsImage() && !st.IsSvgImage()) {
|
||||
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
|
||||
}
|
||||
if err = ctxUser.UploadAvatar(data); err != nil {
|
||||
|
Reference in New Issue
Block a user