mirror of https://github.com/jlelse/GoBlog
Initial profile image support
parent
1b02b400fd
commit
01876592b3
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1000"
|
||||
zoomAndPan="magnify"
|
||||
viewBox="0 0 750 749.999995"
|
||||
height="1000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
version="1.0"
|
||||
id="svg80"
|
||||
sodipodi:docname="GoBlog.svg"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
inkscape:export-filename="GoBlog.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview82"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.804"
|
||||
inkscape:cx="483.20896"
|
||||
inkscape:cy="518.03483"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="991"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg80" />
|
||||
<defs
|
||||
id="defs10">
|
||||
<g
|
||||
id="g2" />
|
||||
<clipPath
|
||||
id="a18b1888e4">
|
||||
<path
|
||||
d="M 227.898438 196.117188 L 528.648438 196.117188 L 528.648438 477.367188 L 227.898438 477.367188 Z M 227.898438 196.117188 "
|
||||
clip-rule="nonzero"
|
||||
id="path4" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="20893c647a">
|
||||
<path
|
||||
d="M 426.003906 358.253906 L 475.503906 358.253906 L 475.503906 407.753906 L 426.003906 407.753906 Z M 426.003906 358.253906 "
|
||||
clip-rule="nonzero"
|
||||
id="path7" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="g222"
|
||||
transform="matrix(1.139191,0,0,1.139191,-55.9257,-82.865789)">
|
||||
<g
|
||||
clip-path="url(#a18b1888e4)"
|
||||
id="g14"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<path
|
||||
stroke-linecap="butt"
|
||||
transform="matrix(-0.750234,0,0,-0.749719,528.6492,477.36703)"
|
||||
fill="none"
|
||||
stroke-linejoin="miter"
|
||||
d="M 0.00101753,-2.11764e-4 H 400.87604 V 375.14045 H 0.00101753 V -2.11764e-4"
|
||||
stroke="#ffffff"
|
||||
stroke-width="16"
|
||||
stroke-opacity="1"
|
||||
stroke-miterlimit="4"
|
||||
id="path12" />
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g22"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(260.2131,406.27794)"
|
||||
id="g20">
|
||||
<g
|
||||
id="g18">
|
||||
<path
|
||||
d="M 147.92187,-84.796875 V -51.9375 c -3.91796,17.992188 -12.48437,31.605469 -25.70312,40.84375 C 109.00781,-1.863281 94.269531,2.75 78,2.75 57.5625,2.75 40.15625,-4.894531 25.78125,-20.1875 11.40625,-35.476562 4.21875,-53.953125 4.21875,-75.609375 c 0,-22.03125 7.035156,-40.628905 21.109375,-55.796875 14.070313,-15.17578 31.628906,-22.76562 52.671875,-22.76562 24.59375,0 44.04688,8.63281 58.35937,25.89062 l -22.56249,24.76562 c -8.08594,-12.96875 -19.343755,-19.45312 -33.781255,-19.45312 -10.648437,0 -19.730469,4.62109 -27.25,13.85937 -7.523437,9.242192 -11.28125,20.406255 -11.28125,33.500005 0,12.84375 3.757813,23.824219 11.28125,32.9375 C 60.285156,-33.554688 69.367188,-29 80.015625,-29 c 8.8125,0 16.488281,-2.660156 23.031255,-7.984375 6.55078,-5.320313 9.82812,-12.382813 9.82812,-21.1875 H 78 v -26.625 z m 0,0"
|
||||
id="path16" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
stroke-linecap="butt"
|
||||
fill="none"
|
||||
stroke-linejoin="miter"
|
||||
d="m 423.85499,183.70806 h 120.7939"
|
||||
stroke="#ffffff"
|
||||
stroke-width="19.785"
|
||||
stroke-opacity="1"
|
||||
stroke-miterlimit="4"
|
||||
id="path24" />
|
||||
<g
|
||||
clip-path="url(#20893c647a)"
|
||||
id="g28"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 450.75391,358.25391 c -13.67188,0 -24.75,11.08203 -24.75,24.75 0,13.66796 11.07812,24.75 24.75,24.75 13.66796,0 24.75,-11.08204 24.75,-24.75 0,-13.66797 -11.08204,-24.75 -24.75,-24.75"
|
||||
fill-opacity="1"
|
||||
fill-rule="nonzero"
|
||||
id="path26" />
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g36"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(254.30363,561.84782)"
|
||||
id="g34">
|
||||
<g
|
||||
id="g32">
|
||||
<path
|
||||
d="m 28.421875,0.742188 c 6.976563,0 12.691406,-3.191407 15.511719,-8.125 L 44.746094,0 h 7.460937 V -28.347656 H 31.167969 v 7.605468 h 11.910156 c -1.039063,7.976563 -7.125,13.0625 -14.65625,13.0625 -11.242187,0 -16.027344,-8.496093 -16.027344,-18.925781 0,-9.71875 4.785157,-18.4375 15.878907,-18.4375 6.679687,0 12.171874,3.1875 14.582031,10.3125 l 8.347656,-3.488281 c -4.230469,-9.792969 -12.539063,-15.0625 -22.855469,-15.0625 -17.328125,0 -25.304687,12.6875 -25.304687,27.046875 0,14.398437 8.125,26.976563 25.378906,26.976563 z m 0,0"
|
||||
id="path30" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g44"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(311.21771,561.84782)"
|
||||
id="g42">
|
||||
<g
|
||||
id="g40">
|
||||
<path
|
||||
d="m 20.890625,0.742188 c 11.464844,0 18.996094,-8.867188 18.996094,-19.367188 0,-10.464844 -7.53125,-19.222656 -18.996094,-19.222656 -11.464844,0 -19,8.757812 -19,19.222656 0,10.5 7.535156,19.367188 19,19.367188 z m 0,-7.792969 c -6.828125,0 -9.574219,-5.492188 -9.574219,-11.574219 0,-6.085938 2.746094,-11.390625 9.574219,-11.390625 6.863281,0 9.570313,5.304687 9.570313,11.390625 0,6.082031 -2.707032,11.574219 -9.570313,11.574219 z m 0,0"
|
||||
id="path38" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g52"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(353.06848,561.84782)"
|
||||
id="g50">
|
||||
<g
|
||||
id="g48">
|
||||
<path
|
||||
d="m 6.753906,0 h 20.445313 c 8.902343,0 17.105469,-5.453125 17.105469,-15.210938 0,-5.234374 -3.636719,-10.949218 -8.425782,-12.839843 2.96875,-1.597657 5.828125,-5.453125 5.828125,-10.242188 0,-10.5 -8.796875,-14.136719 -16.773437,-14.136719 H 6.753906 Z m 9.421875,-31.316406 v -12.617188 h 8.90625 c 4.492188,0 7.976563,2.078125 7.976563,6.160156 0,5.160157 -3.558594,6.457032 -7.234375,6.457032 z m 0,22.335937 v -14.210937 h 10.800781 c 4.933594,0 8.050782,3.191406 8.050782,6.941406 0,4.933594 -2.820313,7.269531 -7.679688,7.269531 z m 0,0"
|
||||
id="path46" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g60"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(400.3361,561.84782)"
|
||||
id="g58">
|
||||
<g
|
||||
id="g56">
|
||||
<path
|
||||
d="m 12.914062,0.742188 c 2.148438,0 4.859376,-0.371094 6.75,-0.742188 v -7.605469 c -0.964843,0.148438 -1.964843,0.222657 -2.484374,0.222657 -1.820313,0 -2.96875,-0.742188 -2.96875,-2.488282 V -52.429688 H 5.15625 v 45.046876 c 0,6.53125 3.488281,8.125 7.757812,8.125 z m 0,0"
|
||||
id="path54" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g68"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(421.5954,561.84782)"
|
||||
id="g66">
|
||||
<g
|
||||
id="g64">
|
||||
<path
|
||||
d="m 20.890625,0.742188 c 11.464844,0 18.996094,-8.867188 18.996094,-19.367188 0,-10.464844 -7.53125,-19.222656 -18.996094,-19.222656 -11.464844,0 -19,8.757812 -19,19.222656 0,10.5 7.535156,19.367188 19,19.367188 z m 0,-7.792969 c -6.828125,0 -9.574219,-5.492188 -9.574219,-11.574219 0,-6.085938 2.746094,-11.390625 9.574219,-11.390625 6.863281,0 9.570313,5.304687 9.570313,11.390625 0,6.082031 -2.707032,11.574219 -9.570313,11.574219 z m 0,0"
|
||||
id="path62" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
fill="#ffffff"
|
||||
fill-opacity="1"
|
||||
id="g76"
|
||||
transform="matrix(1.4641,0,0,1.4641,-175.5567,-186.53194)">
|
||||
<g
|
||||
transform="translate(463.44616,561.84782)"
|
||||
id="g74">
|
||||
<g
|
||||
id="g72">
|
||||
<path
|
||||
d="m 22.335938,-5.789062 c -6.605469,-0.816407 -11.167969,-0.890626 -11.167969,-3.933594 0,-0.851563 0.335937,-1.484375 1.078125,-1.964844 2.113281,0.925781 4.488281,1.445312 7.046875,1.445312 8.128906,0 15.066406,-5.265624 15.066406,-13.542968 0,-2.410156 -0.558594,-4.601563 -1.597656,-6.453125 l 5.382812,-4.34375 -4.714843,-5.75 -5.675782,4.859375 c -2.484375,-1.519532 -5.527344,-2.375 -8.757812,-2.375 -8.199219,0 -15.210938,5.566406 -15.210938,13.617187 0,3.265625 1.148438,6.199219 3.078125,8.535157 -2.1875,1.929687 -3.449219,4.339843 -3.449219,7.644531 0,3.5625 1.445313,5.824219 3.636719,7.308593 L 3.191406,2.746094 c -0.074218,0.371094 -0.148437,0.742187 -0.148437,1.59375 0,7.609375 6.382812,13.210937 17.328125,13.210937 9.203125,0 16.398437,-4.007812 16.398437,-12.394531 0,-8.496094 -7.417969,-10.015625 -14.433593,-10.945312 z m -3.042969,-25.042969 c 3.972656,0 6.085937,2.78125 6.085937,6.675781 0,3.785156 -2.039062,6.53125 -6.085937,6.53125 -4.007813,0 -6.15625,-2.746094 -6.15625,-6.53125 0,-3.894531 2.222656,-6.675781 6.15625,-6.675781 z m 1.152343,41.257812 c -5.308593,0 -9.277343,-1.929687 -9.796874,-5.863281 l 1.894531,-3.117188 c 2.1875,0.519532 4.523437,0.742188 6.601562,1.003907 5.863281,0.667969 9.054688,1.039062 9.054688,3.785156 0,2.671875 -3.042969,4.191406 -7.753907,4.191406 z m 0,0"
|
||||
id="path70" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
stroke-linecap="butt"
|
||||
fill="none"
|
||||
stroke-linejoin="miter"
|
||||
d="M 200.45965,699.9463 H 556.23596"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.58845"
|
||||
stroke-opacity="1"
|
||||
stroke-miterlimit="4"
|
||||
id="path78" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,205 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"go.goblog.app/app/pkgs/bufferpool"
|
||||
"go.goblog.app/app/pkgs/contenttype"
|
||||
)
|
||||
|
||||
type profileImageFormat string
|
||||
|
||||
const (
|
||||
profileImageFormatPNG profileImageFormat = "png"
|
||||
profileImageFormatJPEG profileImageFormat = "jpg"
|
||||
|
||||
profileImagePath = "/profile"
|
||||
profileImagePathJPEG = profileImagePath + "." + string(profileImageFormatJPEG)
|
||||
profileImagePathPNG = profileImagePath + "." + string(profileImageFormatPNG)
|
||||
profileImageSizeRegexPattern = `(?P<width>\d+)(x(?P<height>\d+))?`
|
||||
profileImageCacheName = "profileImage"
|
||||
profileImageHashCacheName = "profileImageHash"
|
||||
|
||||
settingsUpdateProfileImagePath = "/updateprofileimage"
|
||||
settingsDeleteProfileImagePath = "/deleteprofileimage"
|
||||
)
|
||||
|
||||
//go:embed logo/GoBlog.png
|
||||
var defaultLogo []byte
|
||||
|
||||
func (a *goBlog) serveProfileImage(format profileImageFormat) http.HandlerFunc {
|
||||
var mediaType string
|
||||
var encode func(output io.Writer, img *image.NRGBA, quality int) error
|
||||
switch format {
|
||||
case profileImageFormatPNG:
|
||||
mediaType = "image/png"
|
||||
encode = func(output io.Writer, img *image.NRGBA, quality int) error {
|
||||
return imaging.Encode(output, img, imaging.PNG, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
}
|
||||
default:
|
||||
mediaType = "image/jpeg"
|
||||
encode = func(output io.Writer, img *image.NRGBA, quality int) error {
|
||||
return imaging.Encode(output, img, imaging.JPEG, imaging.JPEGQuality(quality))
|
||||
}
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get requested size
|
||||
width, height := 0, 0
|
||||
sizeFormValue := r.FormValue("s")
|
||||
re := regexp.MustCompile(profileImageSizeRegexPattern)
|
||||
if re.MatchString(sizeFormValue) {
|
||||
matches := re.FindStringSubmatch(sizeFormValue)
|
||||
widthIndex := re.SubexpIndex("width")
|
||||
if widthIndex != -1 {
|
||||
width, _ = strconv.Atoi(matches[widthIndex])
|
||||
}
|
||||
heightIndex := re.SubexpIndex("height")
|
||||
if heightIndex != -1 {
|
||||
height, _ = strconv.Atoi(matches[heightIndex])
|
||||
}
|
||||
}
|
||||
if width == 0 || width > 512 {
|
||||
width = 512
|
||||
}
|
||||
if height == 0 || height > 512 {
|
||||
height = width
|
||||
}
|
||||
// Get requested quality
|
||||
quality := 0
|
||||
qualityFormValue := r.FormValue("q")
|
||||
if qualityFormValue != "" {
|
||||
quality, _ = strconv.Atoi(qualityFormValue)
|
||||
}
|
||||
if quality == 0 || quality > 100 {
|
||||
quality = 75
|
||||
}
|
||||
// Read from database
|
||||
var imageBytes []byte
|
||||
if a.hasProfileImage() {
|
||||
var err error
|
||||
imageBytes, err = a.db.retrievePersistentCacheContext(r.Context(), profileImageCacheName)
|
||||
if err != nil || imageBytes == nil {
|
||||
a.serveError(w, r, "Failed to retrieve image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
imageBytes = defaultLogo
|
||||
}
|
||||
// Decode image
|
||||
img, err := imaging.Decode(bytes.NewReader(imageBytes), imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to decode image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Resize image
|
||||
resizedImage := imaging.Fit(img, width, height, imaging.Lanczos)
|
||||
// Encode
|
||||
resizedBuffer := bufferpool.Get()
|
||||
defer bufferpool.Put(resizedBuffer)
|
||||
err = encode(resizedBuffer, resizedImage, quality)
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to encode image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Return
|
||||
w.Header().Set(contentType, mediaType)
|
||||
_, _ = io.Copy(w, resizedBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *goBlog) profileImagePath(format profileImageFormat, size, quality int) string {
|
||||
if !a.hasProfileImage() {
|
||||
return string(profileImagePathJPEG)
|
||||
}
|
||||
query := url.Values{}
|
||||
hashBytes, _ := a.db.retrievePersistentCache(profileImageHashCacheName)
|
||||
query.Set("v", string(hashBytes))
|
||||
if quality != 0 {
|
||||
query.Set("q", fmt.Sprintf("%d", quality))
|
||||
}
|
||||
if size != 0 {
|
||||
query.Set("s", fmt.Sprintf("%d", size))
|
||||
}
|
||||
return fmt.Sprintf("%s.%s?%s", profileImagePath, format, query.Encode())
|
||||
}
|
||||
|
||||
func (a *goBlog) hasProfileImage() bool {
|
||||
a.hasProfileImageInit.Do(func() {
|
||||
a.hasProfileImageBool = a.db.hasPersistantCache(profileImageHashCacheName) && a.db.hasPersistantCache(profileImageCacheName)
|
||||
})
|
||||
return a.hasProfileImageBool
|
||||
}
|
||||
|
||||
func (a *goBlog) serveUpdateProfileImage(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if request is multipart
|
||||
if ct := r.Header.Get(contentType); !strings.Contains(ct, contenttype.MultipartForm) {
|
||||
a.serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(0)
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to parse multipart form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Get file
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to read file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Read the file into temporary buffer and generate sha256 hash
|
||||
hash := sha256.New()
|
||||
buffer := bufferpool.Get()
|
||||
defer bufferpool.Put(buffer)
|
||||
_, _ = io.Copy(io.MultiWriter(buffer, hash), file)
|
||||
_ = file.Close()
|
||||
_ = r.Body.Close()
|
||||
// Cache
|
||||
err = a.db.cachePersistentlyContext(r.Context(), profileImageHashCacheName, []byte(fmt.Sprintf("%x", hash.Sum(nil))))
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to persist hash", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = a.db.cachePersistentlyContext(r.Context(), profileImageCacheName, buffer.Bytes())
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to persist image", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Set bool
|
||||
a.hasProfileImageBool = true
|
||||
// Clear http cache
|
||||
a.cache.purge()
|
||||
// Redirect
|
||||
http.Redirect(w, r, a.profileImagePath(profileImageFormatJPEG, 0, 100), http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *goBlog) serveDeleteProfileImage(w http.ResponseWriter, r *http.Request) {
|
||||
a.hasProfileImageBool = false
|
||||
err := a.db.clearPersistentCache(profileImageHashCacheName)
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to delete hash of profile image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = a.db.clearPersistentCache(profileImageCacheName)
|
||||
if err != nil {
|
||||
a.serveError(w, r, "Failed to delete profile image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
a.cache.purge()
|
||||
// Redirect
|
||||
http.Redirect(w, r, a.profileImagePath(profileImageFormatJPEG, 0, 100), http.StatusFound)
|
||||
}
|
Loading…
Reference in New Issue