From 7b679b38d560ddadea1b06549b215d5a5f3cb0da Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 12:15:52 +0800 Subject: [PATCH 01/29] fix --- custom/conf/app.example.ini | 3 ++ .../config-cheat-sheet.en-us.md | 1 + modules/avatar/avatar.go | 43 +++++++++++------ modules/avatar/avatar_test.go | 47 +++++++++++++++---- modules/setting/picture.go | 13 +++-- services/repository/avatar.go | 8 ++-- services/user/user.go | 7 +-- 7 files changed, 85 insertions(+), 37 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 6e89c42c647c1..82232a43644b7 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1783,6 +1783,9 @@ ROUTER = console ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_FILE_SIZE = 1048576 ;; +;; If the uploaded file is not larger than this size, the image will be used as is, without resizing/converting. +;AVATAR_MAX_ORIGIN_SIZE = 128000 +;; ;; Chinese users can choose "duoshuo" ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ;GRAVATAR_SOURCE = gravatar diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index af6b3a2edd5d7..603577015aa28 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -794,6 +794,7 @@ and - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. +- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index c166f144042a9..6e716b9a25354 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -8,10 +8,10 @@ import ( "fmt" "image" "image/color" + "image/png" _ "image/gif" // for processing gif images _ "image/jpeg" // for processing jpeg images - _ "image/png" // for processing png images "code.gitea.io/gitea/modules/avatar/identicon" "code.gitea.io/gitea/modules/setting" @@ -22,8 +22,9 @@ import ( _ "golang.org/x/image/webp" // for processing webp images ) -// AvatarSize returns avatar's size -const AvatarSize = 290 +// DefaultAvatarSize is used for avatar generation, usually the avatar image saved in server won't be larger than this value. +// Unless the original file is smaller than the resized image. +const DefaultAvatarSize = 256 // RandomImageSize generates and returns a random avatar image unique to input data // in custom size (height and width). @@ -39,26 +40,24 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { // RandomImage generates and returns a random avatar image unique to input data // in default size (height and width). func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(AvatarSize, data) + return RandomImageSize(DefaultAvatarSize, data) } -// Prepare accepts a byte slice as input, validates it contains an image of an -// acceptable format, and crops and resizes it appropriately. -func Prepare(data []byte) (*image.Image, error) { +func resizeAvatar(data []byte) (image.Image, error) { imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("DecodeConfig: %w", err) + return nil, fmt.Errorf("image.DecodeConfig: %w", err) } if imgCfg.Width > setting.Avatar.MaxWidth { - return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) + return nil, fmt.Errorf("image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) } if imgCfg.Height > setting.Avatar.MaxHeight { - return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) + return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) } img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("Decode: %w", err) + return nil, fmt.Errorf("image.Decode: %w", err) } if imgCfg.Width != imgCfg.Height { @@ -74,13 +73,29 @@ func Prepare(data []byte) (*image.Image, error) { img, err = cutter.Crop(img, cutter.Config{ Width: newSize, Height: newSize, - Anchor: image.Point{ax, ay}, + Anchor: image.Point{X: ax, Y: ay}, }) if err != nil { return nil, err } } - img = resize.Resize(AvatarSize, AvatarSize, img, resize.Bilinear) - return &img, nil + img = resize.Resize(DefaultAvatarSize, DefaultAvatarSize, img, resize.Bilinear) + return img, nil +} + +func TryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { + img, err := resizeAvatar(data) + if err != nil { + return nil, err + } + bs := bytes.Buffer{} + if err = png.Encode(&bs, img); err != nil { + return nil, err + } + resized := bs.Bytes() + if len(data) <= int(maxOriginSize) || len(data) <= len(resized) { + return data, nil + } + return resized, nil } diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 5ef4ed379bec8..e13486d828ad7 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -4,6 +4,9 @@ package avatar import ( + "bytes" + "image" + "image/png" "os" "testing" @@ -32,11 +35,11 @@ func Test_PrepareWithPNG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - imgPtr, err := Prepare(data) + img, err := resizeAvatar(data) assert.NoError(t, err) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) + assert.Equal(t, 290, img.Bounds().Max.X) + assert.Equal(t, 290, img.Bounds().Max.Y) } func Test_PrepareWithJPEG(t *testing.T) { @@ -46,18 +49,18 @@ func Test_PrepareWithJPEG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.jpeg") assert.NoError(t, err) - imgPtr, err := Prepare(data) + img, err := resizeAvatar(data) assert.NoError(t, err) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) + assert.Equal(t, 290, img.Bounds().Max.X) + assert.Equal(t, 290, img.Bounds().Max.Y) } func Test_PrepareWithInvalidImage(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 - _, err := Prepare([]byte{}) + _, err := resizeAvatar([]byte{}) assert.EqualError(t, err, "DecodeConfig: image: unknown format") } @@ -68,6 +71,34 @@ func Test_PrepareWithInvalidImageSize(t *testing.T) { data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - _, err = Prepare(data) + _, err = resizeAvatar(data) assert.EqualError(t, err, "Image width is too large: 10 > 5") } + +func Test_TryToResizeAvatar(t *testing.T) { + newImgData := func(size int) []byte { + img := image.NewRGBA(image.Rect(0, 0, size, size)) + bs := bytes.Buffer{} + err := png.Encode(&bs, img) + assert.NoError(t, err) + return bs.Bytes() + } + + // if origin image is smaller than the default size, use the origin image + origin := newImgData(1) + resized, err := TryToResizeAvatar(origin, 0) + assert.NoError(t, err) + assert.Equal(t, origin, resized) + + // use the resized image if the resized is smaller + origin = newImgData(DefaultAvatarSize + 100) + resized, err = TryToResizeAvatar(origin, 0) + assert.NoError(t, err) + assert.Less(t, len(resized), len(origin)) + + // still use the origin image if the origin doesn't exceed the max-origin-size + origin = newImgData(DefaultAvatarSize + 100) + resized, err = TryToResizeAvatar(origin, 1024000) + assert.NoError(t, err) + assert.Equal(t, origin, resized) +} diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 6d7c8b33ce46f..27942b527e898 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -3,20 +3,22 @@ package setting -// settings +// Avatar settings + var ( - // Picture settings Avatar = struct { Storage MaxWidth int MaxHeight int MaxFileSize int64 + MaxOriginSize int64 RenderedSizeFactor int }{ MaxWidth: 4096, MaxHeight: 3072, - MaxFileSize: 1048576, + MaxFileSize: 1024 * 1024, + MaxOriginSize: 128000, RenderedSizeFactor: 3, } @@ -45,7 +47,8 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) - Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) + Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1024 * 1024) + Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(128000) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { @@ -94,5 +97,5 @@ func loadRepoAvatarFrom(rootCfg ConfigProvider) { RepoAvatar.Storage = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec) RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") - RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/assets/img/repo_default.png") + RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png") } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 74e5de877e0ca..e02e568b65ce2 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -6,7 +6,6 @@ package repository import ( "context" "fmt" - "image/png" "io" "strconv" "strings" @@ -15,13 +14,14 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" ) // UploadAvatar saves custom avatar for repository. // FIXME: split uploads to different subdirs in case we have massive number of repos. func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.TryToResizeAvatar(data, setting.Avatar.MaxOriginSize) if err != nil { return err } @@ -47,9 +47,7 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) } if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RepoPath(), newAvatar, err) diff --git a/services/user/user.go b/services/user/user.go index d52a2f404bcf0..a65b66c4f6d63 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -6,7 +6,6 @@ package user import ( "context" "fmt" - "image/png" "io" "time" @@ -244,7 +243,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { // UploadAvatar saves custom avatar for user. func UploadAvatar(u *user_model.User, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.TryToResizeAvatar(data, setting.Avatar.MaxOriginSize) if err != nil { return err } @@ -262,9 +261,7 @@ func UploadAvatar(u *user_model.User, data []byte) error { } if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) From 8f3a71cc0f2d7d5e145033b4e37e47981e3236c0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 13:08:14 +0800 Subject: [PATCH 02/29] support webp animation --- modules/avatar/avatar.go | 26 +++++++++++++++++++++++++- modules/avatar/avatar_test.go | 20 +++++++++++++++++++- modules/avatar/testdata/animated.webp | Bin 0 -> 4934 bytes 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 modules/avatar/testdata/animated.webp diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 6e716b9a25354..d35397523bc10 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -5,6 +5,7 @@ package avatar import ( "bytes" + "errors" "fmt" "image" "image/color" @@ -84,10 +85,33 @@ func resizeAvatar(data []byte) (image.Image, error) { return img, nil } +func isWebp(data []byte) bool { + if len(data) < 12 { + return false + } + if string(data[0:4]) != "RIFF" { + return false + } + if string(data[8:12]) != "WEBP" { + return false + } + return true +} + +func tryToUseOrigin(data []byte, maxOriginSize int64) ([]byte, error) { + if len(data) > int(maxOriginSize) { + return nil, fmt.Errorf("image size is too large and it can't be converted: %d > %d", len(data), maxOriginSize) + } + if isWebp(data) { + return data, nil + } + return nil, errors.New("unsupported image format") +} + func TryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { img, err := resizeAvatar(data) if err != nil { - return nil, err + return tryToUseOrigin(data, maxOriginSize) } bs := bytes.Buffer{} if err = png.Encode(&bs, img); err != nil { diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index e13486d828ad7..f9e1423b157d7 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -98,7 +98,25 @@ func Test_TryToResizeAvatar(t *testing.T) { // still use the origin image if the origin doesn't exceed the max-origin-size origin = newImgData(DefaultAvatarSize + 100) - resized, err = TryToResizeAvatar(origin, 1024000) + resized, err = TryToResizeAvatar(origin, 128000) assert.NoError(t, err) assert.Equal(t, origin, resized) + + // allow to use known image format (eg: webp) if it is small enough + origin, err = os.ReadFile("testdata/animated.webp") + assert.NoError(t, err) + resized, err = TryToResizeAvatar(origin, 128000) + assert.NoError(t, err) + assert.Equal(t, origin, resized) + + // if a format is known, but it's not convertable, then it can't be used + origin, err = os.ReadFile("testdata/animated.webp") + assert.NoError(t, err) + _, err = TryToResizeAvatar(origin, 0) + assert.ErrorContains(t, err, "image size is too large and it can't be converted") + + // do not support unknown image formats, eg: SVG man contain embedded JS + origin = []byte("") + _, err = TryToResizeAvatar(origin, 128000) + assert.ErrorContains(t, err, "unsupported image format") } diff --git a/modules/avatar/testdata/animated.webp b/modules/avatar/testdata/animated.webp new file mode 100644 index 0000000000000000000000000000000000000000..4c05f4695cb66da0d0aa119e31ff721fb212c391 GIT binary patch literal 4934 zcmchacTg1Dw#It^$%tfuL85?U1ONIg*oP8Ipp6 zjN}ZGljJm{={G3%+M(+&_cZNUV`g?@4RNudA%b(^4MRJ5Go!WflzP;SO>R2v<1zs-d0uf-~ktqUr! zGKO$IskQJ1d2O_V(hO;gpSkcQUAOD2AMb{ns1#C*i+R7%=<;5sTC`pa0#oL!zqb&4hT z)x1eFtL-yI+jRYc!w&0m%+=HxD~VR?rMjimgS|8{0SA7tdmxkp$jMCR`rOD!3#23A zuH~XVyfc;I9)n^YjqhvyTCF-sQ!*PGu=_m^eJ6)7FBlr2_jMJbz53|v2^%P+Ld?g$ zhKLPJJ)uIAlTfC;?_6#y@b5TL9xgU7xnZY4CyIoqE?fCwN9qYqcWD|j#fn0jZ#K};Y+ zUc2zm#ojv;vU@)v)hWamwYnBQahI)GZkwC+#j7hd-GMtfUT10%rgLto`pH@*9pRa3 zz9ws2CW!-tsUrSs@goNp$#`pB&BXwCU*_>?-gvb-gQ8lmh~m*)7pj30I|T|PvW+r4 z@7z2Dj}PEugzS($%@1(PfxTce=O}g;CBN6M_DHKRa5{>y<5E~%Ub2Mmmc+7~3HZ4JET zF`}=>jt{n2RhxPqnSrO5)=1wRq%jDn@MFK!*EFZ3)ydV+QXbD$9CrEs1t#P&0WdQe`(8o7eR%A&zQxu>sS;Dun#%tM`h#b>JCLX4TgbIdU zrsRsZ%rU45gDl4c&htfgX$ChC=`%eJZ}B*)Zd5UNwk9|?sO)t-32HT(h>TlrhVg1+g%3a#|5h({K&pyQaQY1Vm*^k*_NcztW?TM&ET6- zIoDr`AvzavyA8*-q=6#R^D36$2IJGUu#0b?zLO?qsAE2A)E;t$$2F;7(z=<Sb;e=+KsF_6psW;U zhim#9YBeV|(KA9Yi`nuQ7m7Ml6K+guNuSvq zT*^c6(uH-Ve%UycEcVQFm(>0c z-g%+=*R4~q6q~)_i)zOTOq^yY`3`!)cPHFM-M{VA-VKm>!NIIFMSG;)!}Ovz?~2}L zKo*YInKOUEF~PyK0sx_Z@Wid@e>k(HNJp*=A+`V#zJv@s6|G%=i>$*y&J-Mqi~r-x z!hd`@Odg*_>|DVzI(x*3lx4!qJ{#2|As4A#PeJdbPy6Zt7r+_ZMY;6QVS01leP7KdSE$7%s-` z#y_i*%PYT^KE!!+1@YOe5XNdMIL(?%G*fs7j1rO!pA04@r_PPHmOWwhrNNBK+|>}} z*bN@cg1GRv2n~hM_bVtX0z+J^dQ&u7Xrnkl$=-QNm5r}>{* z7ItUH`HNE^joVY{#mN{`to01Jx&Qp4I5YO4rrSI3Mg)a_k$KYO#V?#zhkBRi@{9~g zC~HzDm2J-C3DP&Su;ayahzc!rSS-$-cE_H`j$xjvW0zx)sCDol8cacT)xYrJPHRd^ zoBO2PZE^h`A7aOgU#rF}HlsG0tPL-UDp{|IAwl+K7{JW60cKu#Ns|G;nQ5FO=1wGs z$%Tq*i42=$w+W35l2KlM(xVcQp6b&{db(b9Ej@YHjqhoJ@P?VYsBr7MFZ0F$78*#4 zy&~@zMA8q&L}jOvKq!x7UR`3q)eB{4Yo6MI4O`YRu5Spo(;-!VGy};*-@w|M#p`Kr zZVd+=y_|xyu!{k+UPLhN1-OPip45c|eJZXPvoiiUz-z}==$P#~$8{(1cVk-(dKD4( zYI!cx6WR>oe-}Z$w8JkC64vG{r8OfPyg*+Ks@-L{u_jgb^nhV>Tt<%11xA z%pw&fZoYb;mv$H~C!rH~=Dxx13&dM!Udr3qz1wq_5TedtrlumO+s^-yLS=(zm;I0& zNeSNo`%p~dy(yPUsuVC$>LZ_bA4I^KUoU?QSz;uieS3qqj4TQjK=D;VcV9<4F*@L> z&QX3oUchBUm#wdZ7fA_6$;%JVrSDL5_}g;j%N|UUVGTM4s%w<-!KjehHAWV%hfX## z=p?S@H`l8}bcZK~{0LjoP57znnHV*sMsrYUx!^>Rq2!P2T>ZJ01>+|}0x@Dw3Ep?G zPdPfJW??NRk6Dvu-B*P0yp!3f#(avCiYwfyC2aM1s-oUrdmZ|$y9Gb?V&zaY;y!O2 zy}E-ujO5L-a>43k)f>a|F|ln``tqytuG$s_Gp|)};v&6LzY<`Dxkd8=N^05m z7b&dg3Znm7&+y@h|MDGqkL=Q%q4QKqHnoo>3a_LX9UZibvT=>w)M@%LjLrndjB_mJ z5jpp>rR;CciJPHL*kpOyIK%AqNb_v58{mWaM1aF!O~8t0Y{Mvl!d|+P3h>=OaUxXNst!BV0yJL_nw$B3r^?~uH(b@0J-pN@ zptwG4^z+HgJh{zkj*PH*$|QK~mVr9{CevVi;uM^U2IYN)L1NuEtp=~|5z@sM(#u)? zN-W-^{i*Ub^BrxNk4szOozG7e@G=zqBN!yHS?83`+!!ZR&)>BYF1`M%>tXwwa*Hk( zGgEjLQkr5Pq+-hfAqy%)R`uKbQTBhkG3i-#EQo8GWMS0dxOz$Myk5ffJVd)0%CS1z zrcc#sXn|0R*^=drgK?%)fOgbn^H_JxV`Xyk)Q@@Lo!rqtCfs=tZQG;S9+c<+C&`W>unP!uEvR4PJz?PqGnvt|+D3mr;(oAZ)J&z@#K@-Ykz9^ruo&CvirqW zUpq(`Ro?3^N##g8RhI3G1^Fq|mg{d$osjZOg^eQKdNvgn24<8_49ms`l|`n-^a;M9 z-y&Nv*5CPo$Fk&43b9cLsE6%q0*BoOVQ!x6cfJ+*H({oK6UOzmmL^NsYP_@3Y5nR>@hAKSm6Y0itA^MAS3Pj_dI8vPa=0AbuDHM`*@CSbR+$02~z%^t5bGc_1bH3u@MVqk_co zAxHHIcyu=cps^Znpw15t1iMTS$mWJ;_!%U!OzG5OZ?k^rs|ylwHuw5t)9}lR45HJr~I?{I@g%yinw9Wb8Fb zx4sk8TYRrP0wRt|Fw&1lptrJrRLZIIlMp{2u0W!DMXhxCMuJ%JDxvCr+06mpCB|7f WX6FrQg&F20yInV5F>lx6Sp65!k@2$t literal 0 HcmV?d00001 From d40c7e65651c8f9dc36149df06054d86ef1ece93 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 13:40:01 +0800 Subject: [PATCH 03/29] improve webp detection --- modules/avatar/avatar.go | 45 ++++++++++++++++++++++++++++------- modules/avatar/avatar_test.go | 23 +++++++++++------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index d35397523bc10..d11f44a8a74aa 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -85,24 +85,53 @@ func resizeAvatar(data []byte) (image.Image, error) { return img, nil } -func isWebp(data []byte) bool { - if len(data) < 12 { - return false +func detectAcceptableWebp(data []byte) (width, height int, acceptable bool) { + // https://developers.google.com/speed/webp/docs/riff_container + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | WebP file header (12 bytes) | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ChunkHeader('VP8X') (8 bytes) | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Rsv|I|L|E|X|A|R| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Canvas Width Minus One | ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ... Canvas Height Minus One | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + if len(data) < 20 { + return 0, 0, false } if string(data[0:4]) != "RIFF" { - return false + return 0, 0, false } if string(data[8:12]) != "WEBP" { - return false + return 0, 0, false } - return true + if string(data[12:16]) != "VP8X" { + return 0, 0, false + } + + chunk := data[20:] + if len(chunk) < 10 { + return 0, 0, false + } + width = 1 + int(chunk[4]) + int(chunk[5])<<8 + int(chunk[6])<<16 + height = 1 + int(chunk[7]) + int(chunk[8])<<8 + int(chunk[9])<<16 + return width, height, width < setting.Avatar.MaxWidth && height < setting.Avatar.MaxHeight } func tryToUseOrigin(data []byte, maxOriginSize int64) ([]byte, error) { if len(data) > int(maxOriginSize) { - return nil, fmt.Errorf("image size is too large and it can't be converted: %d > %d", len(data), maxOriginSize) + return nil, fmt.Errorf("image data size is too large and it can't be converted: %d > %d", len(data), maxOriginSize) } - if isWebp(data) { + if _, _, ok := detectAcceptableWebp(data); ok { return data, nil } return nil, errors.New("unsupported image format") diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index f9e1423b157d7..faf9e45b2c4ed 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -38,8 +38,8 @@ func Test_PrepareWithPNG(t *testing.T) { img, err := resizeAvatar(data) assert.NoError(t, err) - assert.Equal(t, 290, img.Bounds().Max.X) - assert.Equal(t, 290, img.Bounds().Max.Y) + assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.X) + assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.Y) } func Test_PrepareWithJPEG(t *testing.T) { @@ -52,8 +52,8 @@ func Test_PrepareWithJPEG(t *testing.T) { img, err := resizeAvatar(data) assert.NoError(t, err) - assert.Equal(t, 290, img.Bounds().Max.X) - assert.Equal(t, 290, img.Bounds().Max.Y) + assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.X) + assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.Y) } func Test_PrepareWithInvalidImage(t *testing.T) { @@ -61,7 +61,7 @@ func Test_PrepareWithInvalidImage(t *testing.T) { setting.Avatar.MaxHeight = 5 _, err := resizeAvatar([]byte{}) - assert.EqualError(t, err, "DecodeConfig: image: unknown format") + assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") } func Test_PrepareWithInvalidImageSize(t *testing.T) { @@ -72,10 +72,13 @@ func Test_PrepareWithInvalidImageSize(t *testing.T) { assert.NoError(t, err) _, err = resizeAvatar(data) - assert.EqualError(t, err, "Image width is too large: 10 > 5") + assert.EqualError(t, err, "image width is too large: 10 > 5") } func Test_TryToResizeAvatar(t *testing.T) { + setting.Avatar.MaxWidth = 4096 + setting.Avatar.MaxHeight = 4096 + newImgData := func(size int) []byte { img := image.NewRGBA(image.Rect(0, 0, size, size)) bs := bytes.Buffer{} @@ -111,11 +114,15 @@ func Test_TryToResizeAvatar(t *testing.T) { // if a format is known, but it's not convertable, then it can't be used origin, err = os.ReadFile("testdata/animated.webp") + width, height, acceptable := detectAcceptableWebp(origin) + assert.EqualValues(t, 400, width) + assert.EqualValues(t, 400, height) + assert.True(t, acceptable) assert.NoError(t, err) _, err = TryToResizeAvatar(origin, 0) - assert.ErrorContains(t, err, "image size is too large and it can't be converted") + assert.ErrorContains(t, err, "image data size is too large and it can't be converted") - // do not support unknown image formats, eg: SVG man contain embedded JS + // do not support unknown image formats, eg: SVG may contain embedded JS origin = []byte("") _, err = TryToResizeAvatar(origin, 128000) assert.ErrorContains(t, err, "unsupported image format") From 47029188c2b39765d1f00d9e2d1691d22b99b821 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 14:52:45 +0800 Subject: [PATCH 04/29] fix --- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- modules/setting/picture.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 603577015aa28..8f744beb7ab7f 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -793,7 +793,7 @@ and - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MB): Maximum avatar image file size in bytes. - `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 27942b527e898..bbbf143aa4ca7 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -17,7 +17,7 @@ var ( }{ MaxWidth: 4096, MaxHeight: 3072, - MaxFileSize: 1024 * 1024, + MaxFileSize: 1048576, MaxOriginSize: 128000, RenderedSizeFactor: 3, } @@ -47,8 +47,8 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) - Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1024 * 1024) - Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(128000) + Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) + Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(128000) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { From 34868cefe5a2775647893b999098d7a6e0617c7d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 17:36:13 +0800 Subject: [PATCH 05/29] Update modules/avatar/avatar.go --- modules/avatar/avatar.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index d11f44a8a74aa..0e09ef9507bb7 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -140,6 +140,7 @@ func tryToUseOrigin(data []byte, maxOriginSize int64) ([]byte, error) { func TryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { img, err := resizeAvatar(data) if err != nil { + // in case Golang's package can't decode the image (eg: animated webp), we try to decode by our code to see whether it could be use as origin return tryToUseOrigin(data, maxOriginSize) } bs := bytes.Buffer{} From 90f6e2e70ccd12c3d057b0c935ce69ae04125391 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 17:45:29 +0800 Subject: [PATCH 06/29] remove unnecessary setting access --- modules/avatar/avatar.go | 6 +++++- modules/avatar/avatar_test.go | 12 ++++++------ services/repository/avatar.go | 3 +-- services/user/user.go | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 0e09ef9507bb7..0e89f1377fd38 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -137,7 +137,7 @@ func tryToUseOrigin(data []byte, maxOriginSize int64) ([]byte, error) { return nil, errors.New("unsupported image format") } -func TryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { +func tryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { img, err := resizeAvatar(data) if err != nil { // in case Golang's package can't decode the image (eg: animated webp), we try to decode by our code to see whether it could be use as origin @@ -153,3 +153,7 @@ func TryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { } return resized, nil } + +func TryToResizeAvatar(data []byte) ([]byte, error) { + return tryToUseOrigin(data, setting.Avatar.MaxOriginSize) +} diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index faf9e45b2c4ed..26e00d04f5e95 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -89,26 +89,26 @@ func Test_TryToResizeAvatar(t *testing.T) { // if origin image is smaller than the default size, use the origin image origin := newImgData(1) - resized, err := TryToResizeAvatar(origin, 0) + resized, err := tryToResizeAvatar(origin, 0) assert.NoError(t, err) assert.Equal(t, origin, resized) // use the resized image if the resized is smaller origin = newImgData(DefaultAvatarSize + 100) - resized, err = TryToResizeAvatar(origin, 0) + resized, err = tryToResizeAvatar(origin, 0) assert.NoError(t, err) assert.Less(t, len(resized), len(origin)) // still use the origin image if the origin doesn't exceed the max-origin-size origin = newImgData(DefaultAvatarSize + 100) - resized, err = TryToResizeAvatar(origin, 128000) + resized, err = tryToResizeAvatar(origin, 128000) assert.NoError(t, err) assert.Equal(t, origin, resized) // allow to use known image format (eg: webp) if it is small enough origin, err = os.ReadFile("testdata/animated.webp") assert.NoError(t, err) - resized, err = TryToResizeAvatar(origin, 128000) + resized, err = tryToResizeAvatar(origin, 128000) assert.NoError(t, err) assert.Equal(t, origin, resized) @@ -119,11 +119,11 @@ func Test_TryToResizeAvatar(t *testing.T) { assert.EqualValues(t, 400, height) assert.True(t, acceptable) assert.NoError(t, err) - _, err = TryToResizeAvatar(origin, 0) + _, err = tryToResizeAvatar(origin, 0) assert.ErrorContains(t, err, "image data size is too large and it can't be converted") // do not support unknown image formats, eg: SVG may contain embedded JS origin = []byte("") - _, err = TryToResizeAvatar(origin, 128000) + _, err = tryToResizeAvatar(origin, 128000) assert.ErrorContains(t, err, "unsupported image format") } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index e02e568b65ce2..4d8942f55f1c7 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -14,14 +14,13 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" ) // UploadAvatar saves custom avatar for repository. // FIXME: split uploads to different subdirs in case we have massive number of repos. func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { - avatarData, err := avatar.TryToResizeAvatar(data, setting.Avatar.MaxOriginSize) + avatarData, err := avatar.TryToResizeAvatar(data) if err != nil { return err } diff --git a/services/user/user.go b/services/user/user.go index a65b66c4f6d63..4644d2a859ac5 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -243,7 +243,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { // UploadAvatar saves custom avatar for user. func UploadAvatar(u *user_model.User, data []byte) error { - avatarData, err := avatar.TryToResizeAvatar(data, setting.Avatar.MaxOriginSize) + avatarData, err := avatar.TryToResizeAvatar(data) if err != nil { return err } From ef79c9c17e5c0dc0f5452f2f4cb87b9d1fae6670 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 18:07:12 +0800 Subject: [PATCH 07/29] refactor --- modules/avatar/avatar.go | 89 ++++++++++------------------------- modules/avatar/avatar_test.go | 55 +++++++++------------- services/repository/avatar.go | 2 +- services/user/user.go | 2 +- 4 files changed, 49 insertions(+), 99 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 0e89f1377fd38..4fe82f11353c9 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -44,11 +44,18 @@ func RandomImage(data []byte) (image.Image, error) { return RandomImageSize(DefaultAvatarSize, data) } -func resizeAvatar(data []byte) (image.Image, error) { - imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) +func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { + imgCfg, imgType, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("image.DecodeConfig: %w", err) } + + // for safety, only accept know types explicitly + if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" { + return nil, errors.New("unsupported image type") + } + + // do not process image which is too large, it would consume too much memory if imgCfg.Width > setting.Avatar.MaxWidth { return nil, fmt.Errorf("image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) } @@ -56,6 +63,14 @@ func resizeAvatar(data []byte) (image.Image, error) { return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) } + // if the origin is small enough, just use it, then APNG could be supported + // otherwise, if the image is processed later, APNG loses animation + // and one more thing, webp is not fully supported, image.DecodeConfig works but Decode fails + // so for webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error + if len(data) < int(maxOriginSize) { + return data, nil + } + img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("image.Decode: %w", err) @@ -82,78 +97,22 @@ func resizeAvatar(data []byte) (image.Image, error) { } img = resize.Resize(DefaultAvatarSize, DefaultAvatarSize, img, resize.Bilinear) - return img, nil -} - -func detectAcceptableWebp(data []byte) (width, height int, acceptable bool) { - // https://developers.google.com/speed/webp/docs/riff_container - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - | WebP file header (12 bytes) | - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ChunkHeader('VP8X') (8 bytes) | - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |Rsv|I|L|E|X|A|R| Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Canvas Width Minus One | ... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ... Canvas Height Minus One | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - if len(data) < 20 { - return 0, 0, false - } - if string(data[0:4]) != "RIFF" { - return 0, 0, false - } - if string(data[8:12]) != "WEBP" { - return 0, 0, false - } - if string(data[12:16]) != "VP8X" { - return 0, 0, false - } - chunk := data[20:] - if len(chunk) < 10 { - return 0, 0, false - } - width = 1 + int(chunk[4]) + int(chunk[5])<<8 + int(chunk[6])<<16 - height = 1 + int(chunk[7]) + int(chunk[8])<<8 + int(chunk[9])<<16 - return width, height, width < setting.Avatar.MaxWidth && height < setting.Avatar.MaxHeight -} - -func tryToUseOrigin(data []byte, maxOriginSize int64) ([]byte, error) { - if len(data) > int(maxOriginSize) { - return nil, fmt.Errorf("image data size is too large and it can't be converted: %d > %d", len(data), maxOriginSize) - } - if _, _, ok := detectAcceptableWebp(data); ok { - return data, nil - } - return nil, errors.New("unsupported image format") -} - -func tryToResizeAvatar(data []byte, maxOriginSize int64) ([]byte, error) { - img, err := resizeAvatar(data) - if err != nil { - // in case Golang's package can't decode the image (eg: animated webp), we try to decode by our code to see whether it could be use as origin - return tryToUseOrigin(data, maxOriginSize) - } + // try to encode the cropped/resized image to png bs := bytes.Buffer{} if err = png.Encode(&bs, img); err != nil { return nil, err } resized := bs.Bytes() - if len(data) <= int(maxOriginSize) || len(data) <= len(resized) { + + // usually the png compression is not good enough, use the original image (no cropping/resizing) + if len(data) <= len(resized) { return data, nil } + return resized, nil } -func TryToResizeAvatar(data []byte) ([]byte, error) { - return tryToUseOrigin(data, setting.Avatar.MaxOriginSize) +func ProcessAvatarImage(data []byte) ([]byte, error) { + return processAvatarImage(data, setting.Avatar.MaxOriginSize) } diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 26e00d04f5e95..bd03fc8e7e0ab 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -35,11 +35,8 @@ func Test_PrepareWithPNG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - img, err := resizeAvatar(data) + _, err = processAvatarImage(data, 128000) assert.NoError(t, err) - - assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.X) - assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.Y) } func Test_PrepareWithJPEG(t *testing.T) { @@ -49,18 +46,15 @@ func Test_PrepareWithJPEG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.jpeg") assert.NoError(t, err) - img, err := resizeAvatar(data) + _, err = processAvatarImage(data, 128000) assert.NoError(t, err) - - assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.X) - assert.Equal(t, DefaultAvatarSize, img.Bounds().Max.Y) } func Test_PrepareWithInvalidImage(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 - _, err := resizeAvatar([]byte{}) + _, err := processAvatarImage([]byte{}, 12800) assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") } @@ -71,11 +65,11 @@ func Test_PrepareWithInvalidImageSize(t *testing.T) { data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - _, err = resizeAvatar(data) + _, err = processAvatarImage(data, 12800) assert.EqualError(t, err, "image width is too large: 10 > 5") } -func Test_TryToResizeAvatar(t *testing.T) { +func Test_ProcessAvatarImage(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 @@ -89,41 +83,38 @@ func Test_TryToResizeAvatar(t *testing.T) { // if origin image is smaller than the default size, use the origin image origin := newImgData(1) - resized, err := tryToResizeAvatar(origin, 0) + result, err := processAvatarImage(origin, 0) assert.NoError(t, err) - assert.Equal(t, origin, resized) + assert.Equal(t, origin, result) - // use the resized image if the resized is smaller + // use the result image if the result is smaller origin = newImgData(DefaultAvatarSize + 100) - resized, err = tryToResizeAvatar(origin, 0) + result, err = processAvatarImage(origin, 0) assert.NoError(t, err) - assert.Less(t, len(resized), len(origin)) + assert.Less(t, len(result), len(origin)) // still use the origin image if the origin doesn't exceed the max-origin-size origin = newImgData(DefaultAvatarSize + 100) - resized, err = tryToResizeAvatar(origin, 128000) + result, err = processAvatarImage(origin, 128000) assert.NoError(t, err) - assert.Equal(t, origin, resized) + assert.Equal(t, origin, result) // allow to use known image format (eg: webp) if it is small enough origin, err = os.ReadFile("testdata/animated.webp") assert.NoError(t, err) - resized, err = tryToResizeAvatar(origin, 128000) - assert.NoError(t, err) - assert.Equal(t, origin, resized) - - // if a format is known, but it's not convertable, then it can't be used - origin, err = os.ReadFile("testdata/animated.webp") - width, height, acceptable := detectAcceptableWebp(origin) - assert.EqualValues(t, 400, width) - assert.EqualValues(t, 400, height) - assert.True(t, acceptable) + result, err = processAvatarImage(origin, 128000) assert.NoError(t, err) - _, err = tryToResizeAvatar(origin, 0) - assert.ErrorContains(t, err, "image data size is too large and it can't be converted") + assert.Equal(t, origin, result) // do not support unknown image formats, eg: SVG may contain embedded JS origin = []byte("") - _, err = tryToResizeAvatar(origin, 128000) - assert.ErrorContains(t, err, "unsupported image format") + _, err = processAvatarImage(origin, 128000) + assert.ErrorContains(t, err, "image: unknown format") + + // make sure the canvas size limit works + setting.Avatar.MaxWidth = 5 + setting.Avatar.MaxHeight = 5 + origin = newImgData(10) + _, err = processAvatarImage(origin, 128000) + assert.ErrorContains(t, err, "image width is too large: 10 > 5") } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 4d8942f55f1c7..38c2621bc4d1a 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -20,7 +20,7 @@ import ( // UploadAvatar saves custom avatar for repository. // FIXME: split uploads to different subdirs in case we have massive number of repos. func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { - avatarData, err := avatar.TryToResizeAvatar(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } diff --git a/services/user/user.go b/services/user/user.go index 4644d2a859ac5..5148f2168d585 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -243,7 +243,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { // UploadAvatar saves custom avatar for user. func UploadAvatar(u *user_model.User, data []byte) error { - avatarData, err := avatar.TryToResizeAvatar(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } From ceeee5a965b2ef6d1f38d71f9f1355d1a1a94bfa Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 18:39:48 +0800 Subject: [PATCH 08/29] fine tune --- modules/avatar/avatar.go | 15 ++++++++------- modules/avatar/avatar_test.go | 33 ++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 4fe82f11353c9..5694915d45513 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -50,9 +50,9 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return nil, fmt.Errorf("image.DecodeConfig: %w", err) } - // for safety, only accept know types explicitly + // for safety, only accept known types explicitly if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" { - return nil, errors.New("unsupported image type") + return nil, errors.New("unsupported avatar image type") } // do not process image which is too large, it would consume too much memory @@ -63,10 +63,10 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) } - // if the origin is small enough, just use it, then APNG could be supported - // otherwise, if the image is processed later, APNG loses animation - // and one more thing, webp is not fully supported, image.DecodeConfig works but Decode fails - // so for webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error + // If the origin is small enough, just use it, then APNG could be supported, + // otherwise, if the image is processed later, APNG loses animation. + // And one more thing, webp is not fully supported, image.DecodeConfig works but Decode fails. + // So for webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. if len(data) < int(maxOriginSize) { return data, nil } @@ -76,6 +76,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return nil, fmt.Errorf("image.Decode: %w", err) } + // try to corp and resize the origin image if necessary if imgCfg.Width != imgCfg.Height { var newSize, ax, ay int if imgCfg.Width > imgCfg.Height { @@ -105,7 +106,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { } resized := bs.Bytes() - // usually the png compression is not good enough, use the original image (no cropping/resizing) + // usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller if len(data) <= len(resized) { return data, nil } diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index bd03fc8e7e0ab..7548e3f521d87 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -28,7 +28,7 @@ func Test_RandomImage(t *testing.T) { assert.NoError(t, err) } -func Test_PrepareWithPNG(t *testing.T) { +func Test_ProcessAvatarPNG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 @@ -39,7 +39,7 @@ func Test_PrepareWithPNG(t *testing.T) { assert.NoError(t, err) } -func Test_PrepareWithJPEG(t *testing.T) { +func Test_ProcessAvatarJPEG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 @@ -50,7 +50,7 @@ func Test_PrepareWithJPEG(t *testing.T) { assert.NoError(t, err) } -func Test_PrepareWithInvalidImage(t *testing.T) { +func Test_ProcessAvatarInvalidData(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 @@ -58,7 +58,7 @@ func Test_PrepareWithInvalidImage(t *testing.T) { assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") } -func Test_PrepareWithInvalidImageSize(t *testing.T) { +func Test_ProcessAvatarInvalidImageSize(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 @@ -73,21 +73,36 @@ func Test_ProcessAvatarImage(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 - newImgData := func(size int) []byte { - img := image.NewRGBA(image.Rect(0, 0, size, size)) + newImgData := func(size int, optHeight ...int) []byte { + width := size + height := size + if len(optHeight) == 1 { + height = optHeight[0] + } + img := image.NewRGBA(image.Rect(0, 0, width, height)) bs := bytes.Buffer{} err := png.Encode(&bs, img) assert.NoError(t, err) return bs.Bytes() } - // if origin image is smaller than the default size, use the origin image - origin := newImgData(1) + // if origin image canvas is too large, crop and resize it + origin := newImgData(500, 600) result, err := processAvatarImage(origin, 0) assert.NoError(t, err) + assert.NotEqual(t, origin, result) + decoded, err := png.Decode(bytes.NewReader(result)) + assert.NoError(t, err) + assert.EqualValues(t, DefaultAvatarSize, decoded.Bounds().Max.X) + assert.EqualValues(t, DefaultAvatarSize, decoded.Bounds().Max.Y) + + // if origin image is smaller than the default size, use the origin image + origin = newImgData(1) + result, err = processAvatarImage(origin, 0) + assert.NoError(t, err) assert.Equal(t, origin, result) - // use the result image if the result is smaller + // use the origin image if the origin is smaller origin = newImgData(DefaultAvatarSize + 100) result, err = processAvatarImage(origin, 0) assert.NoError(t, err) From ab70def773455e533cc0d2e623b0441a71f35382 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 21:33:24 +0800 Subject: [PATCH 09/29] improve comment --- modules/avatar/avatar.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 5694915d45513..88698a161ce80 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -44,6 +44,8 @@ func RandomImage(data []byte) (image.Image, error) { return RandomImageSize(DefaultAvatarSize, data) } +// processAvatarImage process the avatar image data, crop and resize it if necessary. +// the returned data could be the original image if no processing is needed. func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { imgCfg, imgType, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { @@ -65,8 +67,8 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { // If the origin is small enough, just use it, then APNG could be supported, // otherwise, if the image is processed later, APNG loses animation. - // And one more thing, webp is not fully supported, image.DecodeConfig works but Decode fails. - // So for webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. + // And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails. + // So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. if len(data) < int(maxOriginSize) { return data, nil } @@ -114,6 +116,8 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return resized, nil } +// ProcessAvatarImage process the avatar image data, crop/ it if necessary. +// the returned data could be the original image if no processing is needed. func ProcessAvatarImage(data []byte) ([]byte, error) { return processAvatarImage(data, setting.Avatar.MaxOriginSize) } From d6e10c536c9f986d91ee2c83730632d59f497115 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 11 May 2023 22:08:09 +0800 Subject: [PATCH 10/29] fix comment typo --- modules/avatar/avatar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 88698a161ce80..15d2ebe2f2ad1 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -78,7 +78,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return nil, fmt.Errorf("image.Decode: %w", err) } - // try to corp and resize the origin image if necessary + // try to crop and resize the origin image if necessary if imgCfg.Width != imgCfg.Height { var newSize, ax, ay int if imgCfg.Width > imgCfg.Height { From 0bfed617637754d4db8d4953cd28da6eb297be5b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 12 May 2023 09:38:54 +0800 Subject: [PATCH 11/29] Apply suggestions from code review Co-authored-by: silverwind --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 82232a43644b7..9cfa7d7bb5c43 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1783,7 +1783,7 @@ ROUTER = console ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_FILE_SIZE = 1048576 ;; -;; If the uploaded file is not larger than this size, the image will be used as is, without resizing/converting. +;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. ;AVATAR_MAX_ORIGIN_SIZE = 128000 ;; ;; Chinese users can choose "duoshuo" diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 8f744beb7ab7f..01de6170700ef 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -794,7 +794,7 @@ and - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **1048576** (1MB): Maximum avatar image file size in bytes. -- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this size, the image will be used as is, without resizing/converting. +- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. From 865f18e71b0879eaa865ff4e5705edf5f4373002 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 12 May 2023 16:03:46 +0800 Subject: [PATCH 12/29] Update docs/content/doc/administration/config-cheat-sheet.en-us.md --- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 01de6170700ef..960317c352482 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -793,7 +793,7 @@ and - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MB): Maximum avatar image file size in bytes. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): Maximum avatar image file size in bytes. - `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. From 9c2eb7a75ea29e2f09e4b06b43655d2cab7d5603 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 12 May 2023 22:15:12 +0800 Subject: [PATCH 13/29] fix typo in comment --- modules/avatar/avatar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 15d2ebe2f2ad1..e855247115837 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -116,7 +116,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { return resized, nil } -// ProcessAvatarImage process the avatar image data, crop/ it if necessary. +// ProcessAvatarImage process the avatar image data, crop and resize it if necessary. // the returned data could be the original image if no processing is needed. func ProcessAvatarImage(data []byte) ([]byte, error) { return processAvatarImage(data, setting.Avatar.MaxOriginSize) From b79d49259d32e007ad8017c63cf3afcc8080a65f Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:22:54 +0200 Subject: [PATCH 14/29] double default AVATAR_MAX_FILE_SIZE to 2MiB --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 4 ++-- docs/content/doc/administration/config-cheat-sheet.zh-cn.md | 2 +- modules/setting/picture.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9cfa7d7bb5c43..72c0cde9d5624 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1781,7 +1781,7 @@ ROUTER = console ;; ;; Maximum allowed file size for uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. -;AVATAR_MAX_FILE_SIZE = 1048576 +;AVATAR_MAX_FILE_SIZE = 2097152 ;; ;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. ;AVATAR_MAX_ORIGIN_SIZE = 128000 diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 960317c352482..52408243266a7 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -793,8 +793,8 @@ and - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): Maximum avatar image file size in bytes. -- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +- `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): Maximum avatar image file size in bytes. +- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 41eed612acc5f..6481e312d95d2 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -215,7 +215,7 @@ menu: - `AVATAR_UPLOAD_PATH`: **data/avatars**: 存储头像的文件系统路径。 - `AVATAR_MAX_WIDTH`: **4096**: 头像最大宽度,单位像素。 - `AVATAR_MAX_HEIGHT`: **3072**: 头像最大高度,单位像素。 -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): 头像最大大小。 +- `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): 头像最大大小。 - `REPOSITORY_AVATAR_STORAGE_TYPE`: **local**: 仓库头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: 存储仓库头像的路径。 diff --git a/modules/setting/picture.go b/modules/setting/picture.go index bbbf143aa4ca7..b9deb5de5826a 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -17,7 +17,7 @@ var ( }{ MaxWidth: 4096, MaxHeight: 3072, - MaxFileSize: 1048576, + MaxFileSize: 2097152, MaxOriginSize: 128000, RenderedSizeFactor: 3, } @@ -47,7 +47,7 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) - Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) + Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(2097152) Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(128000) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) From 85e3fb1c53131cf58828dae31ec50fa616ebe2d6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:25:42 +0200 Subject: [PATCH 15/29] double AVATAR_MAX_ORIGIN_SIZE to 256KiB --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- modules/setting/picture.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 72c0cde9d5624..5c6dc8b612be6 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1784,7 +1784,7 @@ ROUTER = console ;AVATAR_MAX_FILE_SIZE = 2097152 ;; ;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. -;AVATAR_MAX_ORIGIN_SIZE = 128000 +;AVATAR_MAX_ORIGIN_SIZE = 262144 ;; ;; Chinese users can choose "duoshuo" ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 52408243266a7..d1bcaf68809f1 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -794,7 +794,7 @@ and - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): Maximum avatar image file size in bytes. -- `AVATAR_MAX_ORIGIN_SIZE`: **128000** (128KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +- `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. diff --git a/modules/setting/picture.go b/modules/setting/picture.go index b9deb5de5826a..5ee6a97474041 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -18,7 +18,7 @@ var ( MaxWidth: 4096, MaxHeight: 3072, MaxFileSize: 2097152, - MaxOriginSize: 128000, + MaxOriginSize: 262144, RenderedSizeFactor: 3, } @@ -48,7 +48,7 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(2097152) - Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(128000) + Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { From ceb44e54a87180c8d8f23764f45e2360dd9b88ee Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:27:36 +0200 Subject: [PATCH 16/29] reduce AVATAR_RENDERED_SIZE_FACTOR to 2 --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- modules/setting/picture.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5c6dc8b612be6..bfcde02b5eae7 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1777,7 +1777,7 @@ ROUTER = console ;; ;; The multiplication factor for rendered avatar images. ;; Larger values result in finer rendering on HiDPI devices. -;AVATAR_RENDERED_SIZE_FACTOR = 3 +;AVATAR_RENDERED_SIZE_FACTOR = 2 ;; ;; Maximum allowed file size for uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index d1bcaf68809f1..c9b9a0941be65 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -795,7 +795,7 @@ and - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): Maximum avatar image file size in bytes. - `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. -- `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. +- `AVATAR_RENDERED_SIZE_FACTOR`: **2**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 5ee6a97474041..a7720149a8fb7 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -19,7 +19,7 @@ var ( MaxHeight: 3072, MaxFileSize: 2097152, MaxOriginSize: 262144, - RenderedSizeFactor: 3, + RenderedSizeFactor: 2, } GravatarSource string @@ -49,7 +49,7 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(2097152) Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) - Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) + Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": From 60410f966f0cbbf45a92673c5285a6b68104255b Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:29:22 +0200 Subject: [PATCH 17/29] increase AVATAR_MAX_HEIGHT to 4096 --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- docs/content/doc/administration/config-cheat-sheet.zh-cn.md | 2 +- modules/setting/picture.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index bfcde02b5eae7..bd9a74b2904e9 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1773,7 +1773,7 @@ ROUTER = console ;; Max Width and Height of uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_WIDTH = 4096 -;AVATAR_MAX_HEIGHT = 3072 +;AVATAR_MAX_HEIGHT = 4096 ;; ;; The multiplication factor for rendered avatar images. ;; Larger values result in finer rendering on HiDPI devices. diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index c9b9a0941be65..7e2a997325531 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -792,7 +792,7 @@ and - `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. -- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. +- `AVATAR_MAX_HEIGHT`: **4096**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): Maximum avatar image file size in bytes. - `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **2**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 6481e312d95d2..6c1347eed4089 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -214,7 +214,7 @@ menu: - `AVATAR_STORAGE_TYPE`: **local**: 头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `AVATAR_UPLOAD_PATH`: **data/avatars**: 存储头像的文件系统路径。 - `AVATAR_MAX_WIDTH`: **4096**: 头像最大宽度,单位像素。 -- `AVATAR_MAX_HEIGHT`: **3072**: 头像最大高度,单位像素。 +- `AVATAR_MAX_HEIGHT`: **4096**: 头像最大高度,单位像素。 - `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): 头像最大大小。 - `REPOSITORY_AVATAR_STORAGE_TYPE`: **local**: 仓库头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 diff --git a/modules/setting/picture.go b/modules/setting/picture.go index a7720149a8fb7..2c00d368ff203 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -16,7 +16,7 @@ var ( RenderedSizeFactor int }{ MaxWidth: 4096, - MaxHeight: 3072, + MaxHeight: 4096, MaxFileSize: 2097152, MaxOriginSize: 262144, RenderedSizeFactor: 2, @@ -46,7 +46,7 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.Storage = getStorage(rootCfg, "avatars", storageType, avatarSec) Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) - Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) + Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096) Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(2097152) Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2) From 1ac0d77d6f9c698a79a3bc38a5cbf011358ad51a Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:41:51 +0200 Subject: [PATCH 18/29] take into account setting.Avatar.RenderedSizeFactor when scaling, reduce user page avatar size to 256 --- modules/avatar/avatar.go | 10 ++++++---- modules/avatar/avatar_test.go | 9 +++++---- templates/user/profile.tmpl | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index e855247115837..4cd821237890a 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -23,8 +23,9 @@ import ( _ "golang.org/x/image/webp" // for processing webp images ) -// DefaultAvatarSize is used for avatar generation, usually the avatar image saved in server won't be larger than this value. -// Unless the original file is smaller than the resized image. +// DefaultAvatarSize is used for avatar generation, usually the avatar image saved +// in server won't be larger than this value, unless the original file is smaller +// than the resized image. const DefaultAvatarSize = 256 // RandomImageSize generates and returns a random avatar image unique to input data @@ -41,7 +42,7 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { // RandomImage generates and returns a random avatar image unique to input data // in default size (height and width). func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(DefaultAvatarSize, data) + return RandomImageSize(DefaultAvatarSize * setting.Avatar.RenderedSizeFactor, data) } // processAvatarImage process the avatar image data, crop and resize it if necessary. @@ -99,7 +100,8 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { } } - img = resize.Resize(DefaultAvatarSize, DefaultAvatarSize, img, resize.Bilinear) + targetSize := uint(DefaultAvatarSize * setting.Avatar.RenderedSizeFactor) + img = resize.Resize(targetSize, targetSize, img, resize.Bilinear) // try to encode the cropped/resized image to png bs := bytes.Buffer{} diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 7548e3f521d87..c4766bde7f57d 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -72,6 +72,7 @@ func Test_ProcessAvatarInvalidImageSize(t *testing.T) { func Test_ProcessAvatarImage(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 + scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor newImgData := func(size int, optHeight ...int) []byte { width := size @@ -93,8 +94,8 @@ func Test_ProcessAvatarImage(t *testing.T) { assert.NotEqual(t, origin, result) decoded, err := png.Decode(bytes.NewReader(result)) assert.NoError(t, err) - assert.EqualValues(t, DefaultAvatarSize, decoded.Bounds().Max.X) - assert.EqualValues(t, DefaultAvatarSize, decoded.Bounds().Max.Y) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.X) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.Y) // if origin image is smaller than the default size, use the origin image origin = newImgData(1) @@ -103,13 +104,13 @@ func Test_ProcessAvatarImage(t *testing.T) { assert.Equal(t, origin, result) // use the origin image if the origin is smaller - origin = newImgData(DefaultAvatarSize + 100) + origin = newImgData(scaledSize + 100) result, err = processAvatarImage(origin, 0) assert.NoError(t, err) assert.Less(t, len(result), len(origin)) // still use the origin image if the origin doesn't exceed the max-origin-size - origin = newImgData(DefaultAvatarSize + 100) + origin = newImgData(scaledSize + 100) result, err = processAvatarImage(origin, 128000) assert.NoError(t, err) assert.Equal(t, origin, result) diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 08e50fd0cee8d..c1dcd97091a73 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -7,11 +7,11 @@
{{if eq .SignedUserID .ContextUser.ID}} - {{avatar $.Context .ContextUser 290}} + {{avatar $.Context .ContextUser 256}} {{else}} - {{avatar $.Context .ContextUser 290}} + {{avatar $.Context .ContextUser 256}} {{end}}
From c3ac10f60df3249dab8ca2528d5cb626c68961f4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:50:23 +0200 Subject: [PATCH 19/29] revert AVATAR_MAX_FILE_SIZE to 1MiB --- custom/conf/app.example.ini | 2 +- docs/content/doc/administration/config-cheat-sheet.en-us.md | 2 +- docs/content/doc/administration/config-cheat-sheet.zh-cn.md | 2 +- modules/setting/picture.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index bd9a74b2904e9..fc0c502194760 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1781,7 +1781,7 @@ ROUTER = console ;; ;; Maximum allowed file size for uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. -;AVATAR_MAX_FILE_SIZE = 2097152 +;AVATAR_MAX_FILE_SIZE = 1048576 ;; ;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. ;AVATAR_MAX_ORIGIN_SIZE = 262144 diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 7e2a997325531..82665d7d2c649 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -793,7 +793,7 @@ and - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **4096**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): Maximum avatar image file size in bytes. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): Maximum avatar image file size in bytes. - `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. - `AVATAR_RENDERED_SIZE_FACTOR`: **2**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 6c1347eed4089..c672b61598fde 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -215,7 +215,7 @@ menu: - `AVATAR_UPLOAD_PATH`: **data/avatars**: 存储头像的文件系统路径。 - `AVATAR_MAX_WIDTH`: **4096**: 头像最大宽度,单位像素。 - `AVATAR_MAX_HEIGHT`: **4096**: 头像最大高度,单位像素。 -- `AVATAR_MAX_FILE_SIZE`: **2097152** (2MiB): 头像最大大小。 +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): 头像最大大小。 - `REPOSITORY_AVATAR_STORAGE_TYPE`: **local**: 仓库头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: 存储仓库头像的路径。 diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 2c00d368ff203..64d9a608e6510 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -17,7 +17,7 @@ var ( }{ MaxWidth: 4096, MaxHeight: 4096, - MaxFileSize: 2097152, + MaxFileSize: 1048576, MaxOriginSize: 262144, RenderedSizeFactor: 2, } @@ -47,7 +47,7 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096) - Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(2097152) + Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2) From c32812f93e22f06a82e7e2aff78ff97ba61ef101 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 20:53:29 +0200 Subject: [PATCH 20/29] update comment --- modules/avatar/avatar.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 4cd821237890a..5b54249d7d3b0 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -23,9 +23,10 @@ import ( _ "golang.org/x/image/webp" // for processing webp images ) -// DefaultAvatarSize is used for avatar generation, usually the avatar image saved -// in server won't be larger than this value, unless the original file is smaller -// than the resized image. +// DefaultAvatarSize is the target CSS pixel size for avatar generation. It is +// multiplied by setting.Avatar.RenderedSizeFactor and the resulting size is the +// usual size of avatar image saved on server, unless the original file is smaller +// than the size after resizing. const DefaultAvatarSize = 256 // RandomImageSize generates and returns a random avatar image unique to input data From b916c2a3568aed3a16ce65ce7af68ebf3345a218 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 21:16:59 +0200 Subject: [PATCH 21/29] make fmt --- modules/avatar/avatar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 5b54249d7d3b0..10de85b74eb36 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -43,7 +43,7 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { // RandomImage generates and returns a random avatar image unique to input data // in default size (height and width). func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(DefaultAvatarSize * setting.Avatar.RenderedSizeFactor, data) + return RandomImageSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) } // processAvatarImage process the avatar image data, crop and resize it if necessary. From eb3037d4bef4b5577fce28ce2d32170dfd0318b9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 May 2023 21:22:31 +0200 Subject: [PATCH 22/29] replace number in test --- modules/avatar/avatar_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index c4766bde7f57d..a721c77868076 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -35,7 +35,7 @@ func Test_ProcessAvatarPNG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - _, err = processAvatarImage(data, 128000) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) } @@ -46,7 +46,7 @@ func Test_ProcessAvatarJPEG(t *testing.T) { data, err := os.ReadFile("testdata/avatar.jpeg") assert.NoError(t, err) - _, err = processAvatarImage(data, 128000) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) } @@ -111,26 +111,26 @@ func Test_ProcessAvatarImage(t *testing.T) { // still use the origin image if the origin doesn't exceed the max-origin-size origin = newImgData(scaledSize + 100) - result, err = processAvatarImage(origin, 128000) + result, err = processAvatarImage(origin, 262144) assert.NoError(t, err) assert.Equal(t, origin, result) // allow to use known image format (eg: webp) if it is small enough origin, err = os.ReadFile("testdata/animated.webp") assert.NoError(t, err) - result, err = processAvatarImage(origin, 128000) + result, err = processAvatarImage(origin, 262144) assert.NoError(t, err) assert.Equal(t, origin, result) // do not support unknown image formats, eg: SVG may contain embedded JS origin = []byte("") - _, err = processAvatarImage(origin, 128000) + _, err = processAvatarImage(origin, 262144) assert.ErrorContains(t, err, "image: unknown format") // make sure the canvas size limit works setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 origin = newImgData(10) - _, err = processAvatarImage(origin, 128000) + _, err = processAvatarImage(origin, 262144) assert.ErrorContains(t, err, "image width is too large: 10 > 5") } From 2b8133c187e0e9fb709a57df306904256e1dd90c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 13 May 2023 11:18:25 +0800 Subject: [PATCH 23/29] Update templates/user/profile.tmpl --- templates/user/profile.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index c1dcd97091a73..8d172a7d02add 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -7,6 +7,7 @@
{{if eq .SignedUserID .ContextUser.ID}} + {{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}} {{avatar $.Context .ContextUser 256}} {{else}} From f80f89e40987f9df652ef619460ca6061849d82c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 13 May 2023 11:49:59 +0800 Subject: [PATCH 24/29] fix failed tests --- modules/repository/commits_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index a407083f3a891..b6ad967d4ce55 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -6,6 +6,7 @@ package repository import ( "crypto/md5" "fmt" + "strconv" "testing" "time" @@ -136,13 +137,11 @@ func TestPushCommits_AvatarLink(t *testing.T) { enableGravatar(t) assert.Equal(t, - "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s=84", + "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "user2@example.com")) assert.Equal(t, - "https://secure.gravatar.com/avatar/"+ - fmt.Sprintf("%x", md5.Sum([]byte("nonexistent@example.com")))+ - "?d=identicon&s=84", + fmt.Sprintf("https://secure.gravatar.com/avatar/%x?d=identicon&s=%d", md5.Sum([]byte("nonexistent@example.com")), 28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com")) } From 52fb4dbfe337699f5c25ce69fe7a7f2af609d60d Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 13 May 2023 15:38:12 +0200 Subject: [PATCH 25/29] remove fomantic card module and extract all needed styles, fix avatar everywhere --- web_src/css/base.css | 64 +- web_src/css/user.css | 9 +- web_src/fomantic/build/semantic.css | 1378 --------------------------- web_src/fomantic/semantic.json | 1 - 4 files changed, 59 insertions(+), 1393 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 507d92b011460..2accbc2622a69 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1046,16 +1046,59 @@ a.label, box-shadow: -1px -1px 0 0 var(--color-secondary); } +.ui.cards { + display: flex; + margin: -.875em -.5em; + margin-right: -0.5em; + margin-left: -0.5em; + flex-wrap: wrap; +} + .ui.cards > .card, .ui.card { + display: flex; + flex-direction: column; + max-width: 100%; + width: 290px; + margin: .875em .5em; background: var(--color-card); border: 1px solid var(--color-secondary); box-shadow: none; + word-wrap: break-word; } .ui.cards > .card > .content, .ui.card > .content { - border-color: var(--color-secondary); + border-top: 1px solid var(--color-secondary); + max-width: 100%; + padding: 1em; + font-size: 1em; +} + +.ui.cards > .card > .content > .meta + .description, +.ui.cards > .card > .content > .header + .description, +.ui.card > .content > .meta + .description, +.ui.card > .content > .header + .description { + margin-top: .5em; +} + +.ui.cards > .card > .content > .header:not(.ui), +.ui.card > .content > .header:not(.ui) { + font-weight: 500; + font-size: 1.28571429em; + margin-top: -.21425em; + line-height: 1.28571429em; +} + +.ui.cards > .card > .content:first-child, +.ui.card > .content:first-child { + border-top: none; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.cards > .card > :last-child, +.ui.card > :last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); } .ui.cards > .card > .extra, @@ -1102,6 +1145,17 @@ a.ui.card:hover { border-top-color: var(--color-secondary-light-1) !important; } +.ui.three.cards { + margin-left: -1em; + margin-right: -1em; +} + +.ui.three.cards > .card { + width: calc(33.33333333333333% - 2em); + margin-left: 1em; + margin-right: 1em; +} + .ui.comments .comment .text { margin: 0; } @@ -1183,12 +1237,10 @@ a.ui.card:hover { img.ui.avatar, .ui.avatar img, -.ui.avatar svg, -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { +.ui.avatar svg { border-radius: var(--border-radius); + object-fit: contain; + aspect-ratio: 1; } .ui.divided.list > .item { diff --git a/web_src/css/user.css b/web_src/css/user.css index 0a8b49b0387de..8d37db70f1e59 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -38,17 +38,10 @@ width: 100%; } -.user.profile .ui.card #profile-avatar { - background: none; - padding: 1rem 1rem 0.25rem; - justify-content: center; -} .user.profile .ui.card #profile-avatar img { - width: 100%; + max-width: 100%; height: auto; - object-fit: contain; - margin: 0; } @media (max-width: 767px) { diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css index f48201b46a814..33d5311014946 100644 --- a/web_src/fomantic/build/semantic.css +++ b/web_src/fomantic/build/semantic.css @@ -4623,1384 +4623,6 @@ /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Card - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------- - Card ----------------*/ - -.ui.cards > .card, -.ui.card { - max-width: 100%; - position: relative; - display: flex; - flex-direction: column; - width: 290px; - min-height: 0; - background: #FFFFFF; - padding: 0; - border: none; - border-radius: 0.28571429rem; - box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5; - transition: box-shadow 0.1s ease, transform 0.1s ease; - z-index: ''; - word-wrap: break-word; -} - -.ui.card { - margin: 1em 0; -} - -.ui.cards > .card a, -.ui.card a { - cursor: pointer; -} - -.ui.card:first-child { - margin-top: 0; -} - -.ui.card:last-child { - margin-bottom: 0; -} - -/*-------------- - Cards ----------------*/ - -.ui.cards { - display: flex; - margin: -0.875em -0.5em; - flex-wrap: wrap; -} - -.ui.cards > .card { - display: flex; - margin: 0.875em 0.5em; - float: none; -} - -/* Clearing */ - -.ui.cards:after, -.ui.card:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -/* Consecutive Card Groups Preserve Row Spacing */ - -.ui.cards ~ .ui.cards { - margin-top: 0.875em; -} - -/*-------------- - Rounded Edges ----------------*/ - -.ui.cards > .card > :first-child, -.ui.card > :first-child { - border-radius: 0.28571429rem 0.28571429rem 0 0 !important; - border-top: none !important; -} - -.ui.cards > .card > :last-child, -.ui.card > :last-child { - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -.ui.cards > .card > :only-child, -.ui.card > :only-child { - border-radius: 0.28571429rem !important; -} - -/*-------------- - Images ----------------*/ - -.ui.cards > .card > .image, -.ui.card > .image { - position: relative; - display: block; - flex: 0 0 auto; - padding: 0; - background: rgba(0, 0, 0, 0.05); -} - -.ui.cards > .card > .image > img, -.ui.card > .image > img { - display: block; - width: 100%; - height: auto; - border-radius: inherit; -} - -.ui.cards > .card > .image:not(.ui) > img, -.ui.card > .image:not(.ui) > img { - border: none; -} - -/*-------------- - Content ----------------*/ - -.ui.cards > .card > .content, -.ui.card > .content { - flex-grow: 1; - border: none; - border-top: 1px solid rgba(34, 36, 38, 0.1); - background: none; - margin: 0; - padding: 1em 1em; - box-shadow: none; - font-size: 1em; - border-radius: 0; -} - -.ui.cards > .card > .content:after, -.ui.card > .content:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - display: block; - margin: ''; - font-family: var(--fonts-regular); - color: rgba(0, 0, 0, 0.85); -} - -/* Default Header Size */ - -.ui.cards > .card > .content > .header:not(.ui), -.ui.card > .content > .header:not(.ui) { - font-weight: 500; - font-size: 1.28571429em; - margin-top: -0.21425em; - line-height: 1.28571429em; -} - -.ui.cards > .card > .content > .meta + .description, -.ui.cards > .card > .content > .header + .description, -.ui.card > .content > .meta + .description, -.ui.card > .content > .header + .description { - margin-top: 0.5em; -} - -/*---------------- - Floated Content ------------------*/ - -.ui.cards > .card [class*="left floated"], -.ui.card [class*="left floated"] { - float: left; -} - -.ui.cards > .card [class*="right floated"], -.ui.card [class*="right floated"] { - float: right; -} - -/*-------------- - Aligned ----------------*/ - -.ui.cards > .card [class*="left aligned"], -.ui.card [class*="left aligned"] { - text-align: left; -} - -.ui.cards > .card [class*="center aligned"], -.ui.card [class*="center aligned"] { - text-align: center; -} - -.ui.cards > .card [class*="right aligned"], -.ui.card [class*="right aligned"] { - text-align: right; -} - -/*-------------- - Content Image ----------------*/ - -.ui.cards > .card .content img, -.ui.card .content img { - display: inline-block; - vertical-align: middle; - width: ''; -} - -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { - width: 2em; - height: 2em; - border-radius: 500rem; -} - -/*-------------- - Description ----------------*/ - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - clear: both; - color: rgba(0, 0, 0, 0.68); -} - -/*-------------- - Paragraph ----------------*/ - -.ui.cards > .card > .content p, -.ui.card > .content p { - margin: 0 0 0.5em; -} - -.ui.cards > .card > .content p:last-child, -.ui.card > .content p:last-child { - margin-bottom: 0; -} - -/*-------------- - Meta ----------------*/ - -.ui.cards > .card .meta, -.ui.card .meta { - font-size: 1em; - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta *, -.ui.card .meta * { - margin-right: 0.3em; -} - -.ui.cards > .card .meta :last-child, -.ui.card .meta :last-child { - margin-right: 0; -} - -.ui.cards > .card .meta [class*="right floated"], -.ui.card .meta [class*="right floated"] { - margin-right: 0; - margin-left: 0.3em; -} - -/*-------------- - Links ----------------*/ - -/* Generic */ - -.ui.cards > .card > .content a:not(.ui), -.ui.card > .content a:not(.ui) { - color: ''; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content a:not(.ui):hover, -.ui.card > .content a:not(.ui):hover { - color: ''; -} - -/* Header */ - -.ui.cards > .card > .content > a.header, -.ui.card > .content > a.header { - color: rgba(0, 0, 0, 0.85); -} - -.ui.cards > .card > .content > a.header:hover, -.ui.card > .content > a.header:hover { - color: #1e70bf; -} - -/* Meta */ - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: rgba(0, 0, 0, 0.87); -} - -/*-------------- - Buttons ----------------*/ - -.ui.cards > .card > .buttons, -.ui.card > .buttons, -.ui.cards > .card > .button, -.ui.card > .button { - margin: 0 -1px; - width: calc(100% + 2px); -} - -.ui.cards > .card > .buttons:last-child, -.ui.card > .buttons:last-child, -.ui.cards > .card > .button:last-child, -.ui.card > .button:last-child { - margin-bottom: -1px; -} - -/*-------------- - Dimmer ----------------*/ - -.ui.cards > .card .dimmer, -.ui.card .dimmer { - background: ''; - z-index: 10; -} - -/*-------------- - Labels ----------------*/ - -/*-----Star----- */ - -/* Icon */ - -.ui.cards > .card > .content .star.icon, -.ui.card > .content .star.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .star.icon:hover, -.ui.card > .content .star.icon:hover { - opacity: 1; - color: #FFB70A; -} - -.ui.cards > .card > .content .active.star.icon, -.ui.card > .content .active.star.icon { - color: #FFE623; -} - -/*-----Like----- */ - -/* Icon */ - -.ui.cards > .card > .content .like.icon, -.ui.card > .content .like.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .like.icon:hover, -.ui.card > .content .like.icon:hover { - opacity: 1; - color: #FF2733; -} - -.ui.cards > .card > .content .active.like.icon, -.ui.card > .content .active.like.icon { - color: #FF2733; -} - -/*---------------- - Extra Content ------------------*/ - -.ui.cards > .card > .extra, -.ui.card > .extra { - max-width: 100%; - min-height: 0 !important; - flex-grow: 0; - border-top: 1px solid rgba(0, 0, 0, 0.05) !important; - position: static; - background: none; - width: auto; - margin: 0 0; - padding: 0.75em 1em; - top: 0; - left: 0; - color: rgba(0, 0, 0, 0.4); - box-shadow: none; - transition: color 0.1s ease; -} - -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Horizontal - --------------------*/ - -.ui.horizontal.cards > .card, -.ui.card.horizontal { - flex-direction: row; - flex-wrap: wrap; - min-width: 270px; - width: 400px; - max-width: 100%; -} - -.ui.horizontal.cards > .card > .image, -.ui.card.horizontal > .image { - border-radius: 0.28571429rem 0 0 0.28571429rem; - width: 150px; -} - -.ui.horizontal.cards > .card > .image > img, -.ui.card.horizontal > .image > img { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - justify-content: center; - align-items: center; - display: flex; - width: 100%; - height: 100%; - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.horizontal.cards > .card > .image:last-child > img, -.ui.card.horizontal > .image:last-child > img { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -.ui.horizontal.cards > .card > .content, -.ui.horizontal.card > .content { - flex-basis: 1px; -} - -.ui.horizontal.cards > .card > .extra, -.ui.horizontal.card > .extra { - flex-basis: 100%; -} - -/*------------------- - Raised - --------------------*/ - -.ui.raised.cards > .card, -.ui.raised.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.raised.cards a.card:hover, -.ui.link.cards .raised.card:hover, -a.ui.raised.card:hover, -.ui.link.raised.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.15), 0 2px 10px 0 rgba(34, 36, 38, 0.25); -} - -/*------------------- - Centered - --------------------*/ - -.ui.centered.cards { - justify-content: center; -} - -.ui.centered.card { - margin-left: auto; - margin-right: auto; -} - -/*------------------- - Fluid - --------------------*/ - -.ui.fluid.card { - width: 100%; - max-width: 9999px; -} - -/*------------------- - Link - --------------------*/ - -.ui.cards a.card, -.ui.link.cards .card, -a.ui.card, -.ui.link.card { - transform: none; -} - -.ui.cards a.card:hover, -.ui.link.cards .card:not(.icon):hover, -a.ui.card:hover, -.ui.link.card:hover { - cursor: pointer; - z-index: 5; - background: #FFFFFF; - border: none; - box-shadow: 0 1px 3px 0 #BCBDBD, 0 0 0 1px #D4D4D5; - transform: translateY(-3px); -} - -/*------------------- - Colors ---------------------*/ - -.ui.primary.cards > .card, -.ui.cards > .primary.card, -.ui.primary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.primary.cards > .card:hover, -.ui.cards > .primary.card:hover, -.ui.primary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.primary.cards > .card, -.ui.inverted.cards > .primary.card, -.ui.inverted.primary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.primary.cards > .card:hover, -.ui.inverted.cards > .primary.card:hover, -.ui.inverted.primary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.secondary.cards > .card, -.ui.cards > .secondary.card, -.ui.secondary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.secondary.cards > .card:hover, -.ui.cards > .secondary.card:hover, -.ui.secondary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.secondary.cards > .card, -.ui.inverted.cards > .secondary.card, -.ui.inverted.secondary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.secondary.cards > .card:hover, -.ui.inverted.cards > .secondary.card:hover, -.ui.inverted.secondary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6e6e6e, 0 0 0 1px #555555; -} - -.ui.red.cards > .card, -.ui.cards > .red.card, -.ui.red.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #DB2828, 0 1px 3px 0 #D4D4D5; -} - -.ui.red.cards > .card:hover, -.ui.cards > .red.card:hover, -.ui.red.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #d01919, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.red.cards > .card, -.ui.inverted.cards > .red.card, -.ui.inverted.red.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF695E, 0 0 0 1px #555555; -} - -.ui.inverted.red.cards > .card:hover, -.ui.inverted.cards > .red.card:hover, -.ui.inverted.red.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff392b, 0 0 0 1px #555555; -} - -.ui.orange.cards > .card, -.ui.cards > .orange.card, -.ui.orange.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #F2711C, 0 1px 3px 0 #D4D4D5; -} - -.ui.orange.cards > .card:hover, -.ui.cards > .orange.card:hover, -.ui.orange.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #f26202, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.orange.cards > .card, -.ui.inverted.cards > .orange.card, -.ui.inverted.orange.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF851B, 0 0 0 1px #555555; -} - -.ui.inverted.orange.cards > .card:hover, -.ui.inverted.cards > .orange.card:hover, -.ui.inverted.orange.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #e76b00, 0 0 0 1px #555555; -} - -.ui.yellow.cards > .card, -.ui.cards > .yellow.card, -.ui.yellow.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #FBBD08, 0 1px 3px 0 #D4D4D5; -} - -.ui.yellow.cards > .card:hover, -.ui.cards > .yellow.card:hover, -.ui.yellow.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #eaae00, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.yellow.cards > .card, -.ui.inverted.cards > .yellow.card, -.ui.inverted.yellow.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FFE21F, 0 0 0 1px #555555; -} - -.ui.inverted.yellow.cards > .card:hover, -.ui.inverted.cards > .yellow.card:hover, -.ui.inverted.yellow.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ebcd00, 0 0 0 1px #555555; -} - -.ui.olive.cards > .card, -.ui.cards > .olive.card, -.ui.olive.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #B5CC18, 0 1px 3px 0 #D4D4D5; -} - -.ui.olive.cards > .card:hover, -.ui.cards > .olive.card:hover, -.ui.olive.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #a7bd0d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.olive.cards > .card, -.ui.inverted.cards > .olive.card, -.ui.inverted.olive.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D9E778, 0 0 0 1px #555555; -} - -.ui.inverted.olive.cards > .card:hover, -.ui.inverted.cards > .olive.card:hover, -.ui.inverted.olive.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #d2e745, 0 0 0 1px #555555; -} - -.ui.green.cards > .card, -.ui.cards > .green.card, -.ui.green.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #21BA45, 0 1px 3px 0 #D4D4D5; -} - -.ui.green.cards > .card:hover, -.ui.cards > .green.card:hover, -.ui.green.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #16ab39, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.green.cards > .card, -.ui.inverted.cards > .green.card, -.ui.inverted.green.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #2ECC40, 0 0 0 1px #555555; -} - -.ui.inverted.green.cards > .card:hover, -.ui.inverted.cards > .green.card:hover, -.ui.inverted.green.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #1ea92e, 0 0 0 1px #555555; -} - -.ui.teal.cards > .card, -.ui.cards > .teal.card, -.ui.teal.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #00B5AD, 0 1px 3px 0 #D4D4D5; -} - -.ui.teal.cards > .card:hover, -.ui.cards > .teal.card:hover, -.ui.teal.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #009c95, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.teal.cards > .card, -.ui.inverted.cards > .teal.card, -.ui.inverted.teal.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6DFFFF, 0 0 0 1px #555555; -} - -.ui.inverted.teal.cards > .card:hover, -.ui.inverted.cards > .teal.card:hover, -.ui.inverted.teal.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #3affff, 0 0 0 1px #555555; -} - -.ui.blue.cards > .card, -.ui.cards > .blue.card, -.ui.blue.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.blue.cards > .card:hover, -.ui.cards > .blue.card:hover, -.ui.blue.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.blue.cards > .card, -.ui.inverted.cards > .blue.card, -.ui.inverted.blue.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.blue.cards > .card:hover, -.ui.inverted.cards > .blue.card:hover, -.ui.inverted.blue.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.violet.cards > .card, -.ui.cards > .violet.card, -.ui.violet.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #6435C9, 0 1px 3px 0 #D4D4D5; -} - -.ui.violet.cards > .card:hover, -.ui.cards > .violet.card:hover, -.ui.violet.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #5829bb, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.violet.cards > .card, -.ui.inverted.cards > .violet.card, -.ui.inverted.violet.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #A291FB, 0 0 0 1px #555555; -} - -.ui.inverted.violet.cards > .card:hover, -.ui.inverted.cards > .violet.card:hover, -.ui.inverted.violet.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #745aff, 0 0 0 1px #555555; -} - -.ui.purple.cards > .card, -.ui.cards > .purple.card, -.ui.purple.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A333C8, 0 1px 3px 0 #D4D4D5; -} - -.ui.purple.cards > .card:hover, -.ui.cards > .purple.card:hover, -.ui.purple.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #9627ba, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.purple.cards > .card, -.ui.inverted.cards > .purple.card, -.ui.inverted.purple.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DC73FF, 0 0 0 1px #555555; -} - -.ui.inverted.purple.cards > .card:hover, -.ui.inverted.cards > .purple.card:hover, -.ui.inverted.purple.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #cf40ff, 0 0 0 1px #555555; -} - -.ui.pink.cards > .card, -.ui.cards > .pink.card, -.ui.pink.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #E03997, 0 1px 3px 0 #D4D4D5; -} - -.ui.pink.cards > .card:hover, -.ui.cards > .pink.card:hover, -.ui.pink.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #e61a8d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.pink.cards > .card, -.ui.inverted.cards > .pink.card, -.ui.inverted.pink.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF8EDF, 0 0 0 1px #555555; -} - -.ui.inverted.pink.cards > .card:hover, -.ui.inverted.cards > .pink.card:hover, -.ui.inverted.pink.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff5bd1, 0 0 0 1px #555555; -} - -.ui.brown.cards > .card, -.ui.cards > .brown.card, -.ui.brown.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A5673F, 0 1px 3px 0 #D4D4D5; -} - -.ui.brown.cards > .card:hover, -.ui.cards > .brown.card:hover, -.ui.brown.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #975b33, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.brown.cards > .card, -.ui.inverted.cards > .brown.card, -.ui.inverted.brown.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D67C1C, 0 0 0 1px #555555; -} - -.ui.inverted.brown.cards > .card:hover, -.ui.inverted.cards > .brown.card:hover, -.ui.inverted.brown.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #b0620f, 0 0 0 1px #555555; -} - -.ui.grey.cards > .card, -.ui.cards > .grey.card, -.ui.grey.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #767676, 0 1px 3px 0 #D4D4D5; -} - -.ui.grey.cards > .card:hover, -.ui.cards > .grey.card:hover, -.ui.grey.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #838383, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.grey.cards > .card, -.ui.inverted.cards > .grey.card, -.ui.inverted.grey.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DCDDDE, 0 0 0 1px #555555; -} - -.ui.inverted.grey.cards > .card:hover, -.ui.inverted.cards > .grey.card:hover, -.ui.inverted.grey.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #c2c4c5, 0 0 0 1px #555555; -} - -.ui.black.cards > .card, -.ui.cards > .black.card, -.ui.black.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.black.cards > .card:hover, -.ui.cards > .black.card:hover, -.ui.black.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.black.cards > .card, -.ui.inverted.cards > .black.card, -.ui.inverted.black.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.black.cards > .card:hover, -.ui.inverted.cards > .black.card:hover, -.ui.inverted.black.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #000000, 0 0 0 1px #555555; -} - -/*-------------- - Card Count ----------------*/ - -.ui.one.cards { - margin-left: 0; - margin-right: 0; -} - -.ui.one.cards > .card { - width: 100%; -} - -.ui.two.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.two.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.three.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.three.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.four.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.four.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.five.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.five.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.six.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.six.cards > .card { - width: calc(16.666666666666664% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.seven.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.seven.cards > .card { - width: calc(14.285714285714285% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -.ui.eight.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.eight.cards > .card { - width: calc(12.5% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 11px; -} - -.ui.nine.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.nine.cards > .card { - width: calc(11.11111111111111% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 10px; -} - -.ui.ten.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.ten.cards > .card { - width: calc(10% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -/*------------------- - Doubling - --------------------*/ - -/* Mobile Only */ - -@media only screen and (max-width: 767.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.seven.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.seven.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.nine.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.nine.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.ten.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.ten.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } -} - -/* Tablet Only */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.eight.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.nine.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.nine.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.ten.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.ten.doubling.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } -} - -/*------------------- - Stackable - --------------------*/ - -@media only screen and (max-width: 767.98px) { - .ui.stackable.cards { - display: block !important; - } - - .ui.stackable.cards .card:first-child { - margin-top: 0 !important; - } - - .ui.stackable.cards > .card { - display: block !important; - height: auto !important; - margin: 1em 1em; - padding: 0 !important; - width: calc(100% - 2em) !important; - } -} - -/*-------------- - Size ----------------*/ - -.ui.cards > .card { - font-size: 1em; -} - -.ui.mini.cards .card { - font-size: 0.78571429rem; -} - -.ui.tiny.cards .card { - font-size: 0.85714286rem; -} - -.ui.small.cards .card { - font-size: 0.92857143rem; -} - -.ui.large.cards .card { - font-size: 1.14285714rem; -} - -.ui.big.cards .card { - font-size: 1.28571429rem; -} - -.ui.huge.cards .card { - font-size: 1.42857143rem; -} - -.ui.massive.cards .card { - font-size: 1.71428571rem; -} - -/*----------------- - Inverted - ------------------*/ - -.ui.inverted.cards > .card, -.ui.inverted.card { - background: #1B1C1D; - box-shadow: 0 1px 3px 0 #555555, 0 0 0 1px #555555; -} - -/* Content */ - -.ui.inverted.cards > .card > .content, -.ui.inverted.card > .content { - border-top: 1px solid rgba(255, 255, 255, 0.15); -} - -/* Header */ - -.ui.inverted.cards > .card > .content > .header, -.ui.inverted.card > .content > .header { - color: rgba(255, 255, 255, 0.9); -} - -/* Description */ - -.ui.inverted.cards > .card > .content > .description, -.ui.inverted.card > .content > .description { - color: rgba(255, 255, 255, 0.8); -} - -/* Meta */ - -.ui.inverted.cards > .card .meta, -.ui.inverted.card .meta { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui), -.ui.inverted.card .meta > a:not(.ui) { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui):hover, -.ui.inverted.card .meta > a:not(.ui):hover { - color: #ffffff; -} - -/* Extra */ - -.ui.inverted.cards > .card > .extra, -.ui.inverted.card > .extra { - border-top: 1px solid rgba(255, 255, 255, 0.15) !important; - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card > .extra a:not(.ui), -.ui.inverted.card > .extra a:not(.ui) { - color: rgba(255, 255, 255, 0.5); -} - -.ui.inverted.cards > .card > .extra a:not(.ui):hover, -.ui.inverted.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/* Link card(s) */ - -.ui.inverted.cards a.card:hover, -.ui.inverted.link.cards .card:not(.icon):hover, -a.inverted.ui.card:hover, -.ui.inverted.link.card:hover { - background: #1B1C1D; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /*! * # Fomantic-UI - Checkbox * http://github.com/fomantic/Fomantic-UI/ diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index 738f53d2976e0..4a516b2c3ae3c 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -23,7 +23,6 @@ "components": [ "api", "button", - "card", "checkbox", "comment", "container", From 68047ce23589b0ffff2311406ebe6d31fc78c052 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 13 May 2023 15:46:31 +0200 Subject: [PATCH 26/29] add comment --- web_src/css/base.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web_src/css/base.css b/web_src/css/base.css index 2accbc2622a69..5057e5d62d5e9 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1046,6 +1046,10 @@ a.label, box-shadow: -1px -1px 0 0 var(--color-secondary); } +/* Below styles are a skeleton of the full fomantic styles which */ +/* are needed to get all current uses of fomantic cards working */ +/* TODO: remove all these styles and use custom styling instead */ + .ui.cards { display: flex; margin: -.875em -.5em; From 528839180051b55248862e6cb3c0d6af34286bb8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 13 May 2023 15:57:25 +0200 Subject: [PATCH 27/29] avatar page fixes --- web_src/css/base.css | 11 ++++++++++- web_src/css/user.css | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 5057e5d62d5e9..9eb2dc5968ade 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1058,13 +1058,22 @@ a.label, flex-wrap: wrap; } +.ui.card:last-child { + margin-bottom: 0; +} +.ui.card:first-child { + margin-top: 0; +} + .ui.cards > .card, .ui.card { display: flex; flex-direction: column; max-width: 100%; width: 290px; - margin: .875em .5em; + margin: 1em 0; + min-height: 0; + padding: 0; background: var(--color-card); border: 1px solid var(--color-secondary); box-shadow: none; diff --git a/web_src/css/user.css b/web_src/css/user.css index 8d37db70f1e59..648480d71d04c 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -38,6 +38,10 @@ width: 100%; } +.user.profile .ui.card #profile-avatar { + padding: 1rem 1rem 0.25rem; + justify-content: center; +} .user.profile .ui.card #profile-avatar img { max-width: 100%; From 79415191d0018f3ad58c06bfb4c6000ace7ab101 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 13 May 2023 16:02:01 +0200 Subject: [PATCH 28/29] more fixes, migrate is now pixel-identical --- web_src/css/base.css | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 9eb2dc5968ade..9478497c3c954 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1050,14 +1050,6 @@ a.label, /* are needed to get all current uses of fomantic cards working */ /* TODO: remove all these styles and use custom styling instead */ -.ui.cards { - display: flex; - margin: -.875em -.5em; - margin-right: -0.5em; - margin-left: -0.5em; - flex-wrap: wrap; -} - .ui.card:last-child { margin-bottom: 0; } @@ -1071,7 +1063,6 @@ a.label, flex-direction: column; max-width: 100%; width: 290px; - margin: 1em 0; min-height: 0; padding: 0; background: var(--color-card); @@ -1080,6 +1071,22 @@ a.label, word-wrap: break-word; } +.ui.card { + margin: 1em 0; +} + +.ui.cards { + display: flex; + margin: -0.875em -0.5em; + flex-wrap: wrap; +} + +.ui.cards > .card { + display: flex; + margin: 0.875em 0.5em; + float: none; +} + .ui.cards > .card > .content, .ui.card > .content { border-top: 1px solid var(--color-secondary); @@ -1114,6 +1121,11 @@ a.label, border-radius: 0 0 var(--border-radius) var(--border-radius); } +.ui.cards > .card > :only-child, +.ui.card > :only-child { + border-radius: var(--border-radius) !important; +} + .ui.cards > .card > .extra, .ui.card > .extra, .ui.cards > .card > .extra a:not(.ui), From 04c5fdefa81502add6911b8afe122ee77d1a1d58 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 13 May 2023 19:19:55 +0200 Subject: [PATCH 29/29] move card styles to own file --- web_src/css/base.css | 135 ----------------------------------- web_src/css/index.css | 1 + web_src/css/modules/card.css | 134 ++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 135 deletions(-) create mode 100644 web_src/css/modules/card.css diff --git a/web_src/css/base.css b/web_src/css/base.css index 9478497c3c954..6c1bbb00c467e 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1046,141 +1046,6 @@ a.label, box-shadow: -1px -1px 0 0 var(--color-secondary); } -/* Below styles are a skeleton of the full fomantic styles which */ -/* are needed to get all current uses of fomantic cards working */ -/* TODO: remove all these styles and use custom styling instead */ - -.ui.card:last-child { - margin-bottom: 0; -} -.ui.card:first-child { - margin-top: 0; -} - -.ui.cards > .card, -.ui.card { - display: flex; - flex-direction: column; - max-width: 100%; - width: 290px; - min-height: 0; - padding: 0; - background: var(--color-card); - border: 1px solid var(--color-secondary); - box-shadow: none; - word-wrap: break-word; -} - -.ui.card { - margin: 1em 0; -} - -.ui.cards { - display: flex; - margin: -0.875em -0.5em; - flex-wrap: wrap; -} - -.ui.cards > .card { - display: flex; - margin: 0.875em 0.5em; - float: none; -} - -.ui.cards > .card > .content, -.ui.card > .content { - border-top: 1px solid var(--color-secondary); - max-width: 100%; - padding: 1em; - font-size: 1em; -} - -.ui.cards > .card > .content > .meta + .description, -.ui.cards > .card > .content > .header + .description, -.ui.card > .content > .meta + .description, -.ui.card > .content > .header + .description { - margin-top: .5em; -} - -.ui.cards > .card > .content > .header:not(.ui), -.ui.card > .content > .header:not(.ui) { - font-weight: 500; - font-size: 1.28571429em; - margin-top: -.21425em; - line-height: 1.28571429em; -} - -.ui.cards > .card > .content:first-child, -.ui.card > .content:first-child { - border-top: none; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.ui.cards > .card > :last-child, -.ui.card > :last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); -} - -.ui.cards > .card > :only-child, -.ui.card > :only-child { - border-radius: var(--border-radius) !important; -} - -.ui.cards > .card > .extra, -.ui.card > .extra, -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: var(--color-text); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: var(--color-primary); -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - color: var(--color-text); -} - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - color: var(--color-text); -} - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: var(--color-text-light-2); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: var(--color-text); -} - -.ui.cards a.card:hover, -a.ui.card:hover { - border: 1px solid var(--color-secondary); - background: var(--color-card); -} - -.ui.cards > .card > .extra, -.ui.card > .extra { - color: var(--color-text); - border-top-color: var(--color-secondary-light-1) !important; -} - -.ui.three.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.three.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; -} - .ui.comments .comment .text { margin: 0; } diff --git a/web_src/css/index.css b/web_src/css/index.css index 6fb92f2ecb21c..1723de3a2d403 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -10,6 +10,7 @@ @import "./modules/tippy.css"; @import "./modules/modal.css"; @import "./modules/breadcrumb.css"; +@import "./modules/card.css"; @import "./code/linebutton.css"; @import "./markup/content.css"; @import "./markup/codecopy.css"; diff --git a/web_src/css/modules/card.css b/web_src/css/modules/card.css new file mode 100644 index 0000000000000..c0f7e83037ca2 --- /dev/null +++ b/web_src/css/modules/card.css @@ -0,0 +1,134 @@ +/* Below styles are a subset of the full fomantic card styles which are */ +/* needed to get all current uses of fomantic cards working. */ +/* TODO: remove all these styles and use custom styling instead */ + +.ui.card:last-child { + margin-bottom: 0; +} +.ui.card:first-child { + margin-top: 0; +} + +.ui.cards > .card, +.ui.card { + display: flex; + flex-direction: column; + max-width: 100%; + width: 290px; + min-height: 0; + padding: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + box-shadow: none; + word-wrap: break-word; +} + +.ui.card { + margin: 1em 0; +} + +.ui.cards { + display: flex; + margin: -0.875em -0.5em; + flex-wrap: wrap; +} + +.ui.cards > .card { + display: flex; + margin: 0.875em 0.5em; + float: none; +} + +.ui.cards > .card > .content, +.ui.card > .content { + border-top: 1px solid var(--color-secondary); + max-width: 100%; + padding: 1em; + font-size: 1em; +} + +.ui.cards > .card > .content > .meta + .description, +.ui.cards > .card > .content > .header + .description, +.ui.card > .content > .meta + .description, +.ui.card > .content > .header + .description { + margin-top: .5em; +} + +.ui.cards > .card > .content > .header:not(.ui), +.ui.card > .content > .header:not(.ui) { + font-weight: 500; + font-size: 1.28571429em; + margin-top: -.21425em; + line-height: 1.28571429em; +} + +.ui.cards > .card > .content:first-child, +.ui.card > .content:first-child { + border-top: none; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.cards > .card > :last-child, +.ui.card > :last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +.ui.cards > .card > :only-child, +.ui.card > :only-child { + border-radius: var(--border-radius) !important; +} + +.ui.cards > .card > .extra, +.ui.card > .extra, +.ui.cards > .card > .extra a:not(.ui), +.ui.card > .extra a:not(.ui) { + color: var(--color-text); +} + +.ui.cards > .card > .extra a:not(.ui):hover, +.ui.card > .extra a:not(.ui):hover { + color: var(--color-primary); +} + +.ui.cards > .card > .content > .header, +.ui.card > .content > .header { + color: var(--color-text); +} + +.ui.cards > .card > .content > .description, +.ui.card > .content > .description { + color: var(--color-text); +} + +.ui.cards > .card .meta > a:not(.ui), +.ui.card .meta > a:not(.ui) { + color: var(--color-text-light-2); +} + +.ui.cards > .card .meta > a:not(.ui):hover, +.ui.card .meta > a:not(.ui):hover { + color: var(--color-text); +} + +.ui.cards a.card:hover, +a.ui.card:hover { + border: 1px solid var(--color-secondary); + background: var(--color-card); +} + +.ui.cards > .card > .extra, +.ui.card > .extra { + color: var(--color-text); + border-top-color: var(--color-secondary-light-1) !important; +} + +.ui.three.cards { + margin-left: -1em; + margin-right: -1em; +} + +.ui.three.cards > .card { + width: calc(33.33333333333333% - 2em); + margin-left: 1em; + margin-right: 1em; +}