Have got the processor plumbing working
This commit is contained in:
parent
036b683eab
commit
599c72d465
|
|
@ -47,6 +47,13 @@ export default class UploadEditController extends Controller {
|
|||
this._createSession();
|
||||
}
|
||||
|
||||
async addProcessor(ev) {
|
||||
ev.preventDefault();
|
||||
await this._addProcessor({
|
||||
type: "shadow"
|
||||
});
|
||||
}
|
||||
|
||||
_rebuildProcessList() {
|
||||
let el = this.processListTarget;
|
||||
|
||||
|
|
@ -66,6 +73,10 @@ export default class UploadEditController extends Controller {
|
|||
try {
|
||||
let resp = await fetch(`/sites/${this.siteIdValue}/imageedit/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"base_upload": this.uploadIdValue,
|
||||
})
|
||||
|
|
@ -79,4 +90,22 @@ export default class UploadEditController extends Controller {
|
|||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _addProcessor(processor) {
|
||||
try {
|
||||
let resp = await fetch(`/sites/${this.siteIdValue}/imageedit/${this._state.session.guid}/processors`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(processor)
|
||||
});
|
||||
|
||||
this._state = await resp.json();
|
||||
this.previewTarget.src = this._state.preview_url;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,6 +153,7 @@ Starting weiro without any arguments will start the server.
|
|||
siteGroup.Get("/uploads/:uploadID/edit", uh.Edit)
|
||||
|
||||
siteGroup.Post("/imageedit", ieh.Create)
|
||||
siteGroup.Post("/imageedit/:sessionID/processors", ieh.AddProcessor)
|
||||
siteGroup.Get("/imageedit/:sessionID/preview/:versionID", ieh.Preview)
|
||||
|
||||
siteGroup.Get("/settings", ssh.General)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ type ImageEditHandlers struct {
|
|||
ImageEditService *imgedit.Service
|
||||
}
|
||||
|
||||
type sessionResponse struct {
|
||||
Session *models.ImageEditSession `json:"session"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
}
|
||||
|
||||
func (ieh ImageEditHandlers) Create(c fiber.Ctx) error {
|
||||
var req struct {
|
||||
BaseUploadID int64 `json:"base_upload"`
|
||||
|
|
@ -29,10 +34,7 @@ func (ieh ImageEditHandlers) Create(c fiber.Ctx) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var resp = struct {
|
||||
Session models.ImageEditSession `json:"session"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
}{
|
||||
var resp = sessionResponse{
|
||||
Session: res,
|
||||
PreviewURL: res.PreviewURL(),
|
||||
}
|
||||
|
|
@ -65,3 +67,27 @@ func (ieh ImageEditHandlers) Preview(c fiber.Ctx) error {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ieh ImageEditHandlers) AddProcessor(c fiber.Ctx) error {
|
||||
sessionID := c.Params("sessionID")
|
||||
if sessionID == "" {
|
||||
log.Println("No session ID")
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
var req imgedit.AddProcessorReq
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
log.Printf("Failed to parse request body: %v", err)
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
res, err := ieh.ImageEditService.AddProcessor(c.Context(), sessionID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).JSON(sessionResponse{
|
||||
Session: res,
|
||||
PreviewURL: res.PreviewURL(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
|
|
@ -37,6 +38,13 @@ func (h IndexHandler) Index(c fiber.Ctx) error {
|
|||
}
|
||||
}
|
||||
|
||||
sess := session.FromContext(c)
|
||||
lastSiteID, ok := sess.Get("last_site_id").(int64)
|
||||
log.Printf("last site id: %v", lastSiteID)
|
||||
if ok {
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", lastSiteID))
|
||||
}
|
||||
|
||||
site, err := h.SiteService.BestSite(c.Context(), user)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"emperror.dev/errors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/session"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
"lmika.dev/lmika/weiro/services/sites"
|
||||
|
|
@ -41,6 +42,9 @@ func RequiresSite(sites *sites.Service) func(c fiber.Ctx) error {
|
|||
}
|
||||
c.Locals("allSites", sitesOwnedByUser)
|
||||
|
||||
sess := session.FromContext(c)
|
||||
sess.Set("last_site_id", siteID)
|
||||
|
||||
if pubTargets, err := sites.BestPubTarget(c.Context(), site); err == nil {
|
||||
c.Locals("pubTarget", pubTargets)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ func (ieh *ImageEditSession) RecalcVersionIDs() {
|
|||
}
|
||||
|
||||
type ImageEditProcessor struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Props json.RawMessage `json:"props"`
|
||||
|
||||
|
|
@ -46,6 +47,8 @@ type ImageEditProcessor struct {
|
|||
|
||||
func (ieh *ImageEditProcessor) SetVersionID(previousVersionID string) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(ieh.ID)
|
||||
sb.WriteString("-")
|
||||
sb.WriteString(previousVersionID)
|
||||
sb.WriteString("-")
|
||||
sb.WriteString(ieh.Type)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ import (
|
|||
"lmika.dev/lmika/weiro/models"
|
||||
)
|
||||
|
||||
func (s *Service) reprocess(ctx context.Context, session models.ImageEditSession) (imageSource, error) {
|
||||
func (s *Service) reprocess(ctx context.Context, session *models.ImageEditSession) (imageSource, error) {
|
||||
var img imageSource
|
||||
|
||||
for _, p := range session.Processors {
|
||||
|
|
@ -68,6 +69,10 @@ func (s *Service) processImage(ctx context.Context, srcImg image.Image, processo
|
|||
defer f.Close()
|
||||
|
||||
return imaging.Decode(f)
|
||||
case "shadow":
|
||||
shadow := makeBoxShadow(srcImg, color.Black, 4, 10, 0)
|
||||
composit := imaging.OverlayCenter(shadow, srcImg, 1.0)
|
||||
return composit, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown processor type: %v", processor.Type)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,31 +27,15 @@ func New(
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) LoadImageVersion(ctx context.Context, sessionID string, versionID string) (mimeType string, rw func() (io.ReadCloser, error), err error) {
|
||||
func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (*models.ImageEditSession, error) {
|
||||
site, user, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
session, err := s.sessionStore.get(sessionID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
} else if session.SiteID != site.ID || session.UserID != user.ID {
|
||||
return "", nil, models.PermissionError
|
||||
}
|
||||
|
||||
return s.sessionStore.getImage(session, versionID+"."+session.ImageExt)
|
||||
}
|
||||
|
||||
func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (models.ImageEditSession, error) {
|
||||
site, user, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upload, _, err := s.uploadService.OpenUpload(ctx, baseUploadID)
|
||||
if err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ext string
|
||||
|
|
@ -61,7 +45,7 @@ func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (models.Im
|
|||
case "image/png":
|
||||
ext = "png"
|
||||
default:
|
||||
return models.ImageEditSession{}, models.UnsupportedImageFormat
|
||||
return nil, models.UnsupportedImageFormat
|
||||
}
|
||||
|
||||
newSession := models.ImageEditSession{
|
||||
|
|
@ -74,6 +58,7 @@ func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (models.Im
|
|||
UpdatedAt: time.Now().UTC(),
|
||||
Processors: []models.ImageEditProcessor{
|
||||
{
|
||||
ID: models.NewNanoID(),
|
||||
Type: "copy-upload",
|
||||
Props: mustToJSON(models.CopyUploadProps{UploadID: baseUploadID}),
|
||||
},
|
||||
|
|
@ -81,15 +66,67 @@ func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (models.Im
|
|||
}
|
||||
|
||||
newSession.RecalcVersionIDs()
|
||||
if err := s.sessionStore.create(newSession); err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
if err := s.sessionStore.save(&newSession); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := s.reprocess(ctx, newSession); err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
if _, err := s.reprocess(ctx, &newSession); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newSession, nil
|
||||
return &newSession, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoadImageVersion(ctx context.Context, sessionID string, versionID string) (mimeType string, rw func() (io.ReadCloser, error), err error) {
|
||||
session, err := s.loadAndVerifySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return s.sessionStore.getImage(session, versionID+"."+session.ImageExt)
|
||||
}
|
||||
|
||||
type AddProcessorReq struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (s *Service) AddProcessor(ctx context.Context, sessionID string, req AddProcessorReq) (*models.ImageEditSession, error) {
|
||||
session, err := s.loadAndVerifySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: verify processor, etc.
|
||||
session.Processors = append(session.Processors, models.ImageEditProcessor{
|
||||
ID: models.NewNanoID(),
|
||||
Type: req.Type,
|
||||
})
|
||||
|
||||
session.RecalcVersionIDs()
|
||||
if err := s.sessionStore.save(session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := s.reprocess(ctx, session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadAndVerifySession(ctx context.Context, sessionID string) (*models.ImageEditSession, error) {
|
||||
site, user, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := s.sessionStore.get(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if session.SiteID != site.ID || session.UserID != user.ID {
|
||||
return nil, models.PermissionError
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchSiteAndUser(ctx context.Context) (models.Site, models.User, error) {
|
||||
|
|
|
|||
35
services/imgedit/shadow.go
Normal file
35
services/imgedit/shadow.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package imgedit
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
func makeBoxShadow(maskImg image.Image, shadowColor color.Color, sigma float64, shadowMargin, offsetY int) image.Image {
|
||||
w, h := maskImg.Bounds().Dx(), maskImg.Bounds().Dy()
|
||||
cr, cg, cb, _ := shadowColor.RGBA()
|
||||
cr8, cg8, cb8 := uint8(cr>>8), uint8(cg>>8), uint8(cb>>8)
|
||||
|
||||
// New box image
|
||||
backing := image.NewNRGBA(image.Rect(0, 0, w+shadowMargin*2, h+shadowMargin*2+offsetY))
|
||||
newImg := image.NewNRGBA(image.Rect(0, 0, w+shadowMargin*2, h+shadowMargin*2+offsetY))
|
||||
for x := 0; x < w+shadowMargin*2; x++ {
|
||||
for y := 0; y < h+shadowMargin*2; y++ {
|
||||
var c = color.NRGBA{R: 255, G: 255, B: 255, A: 0}
|
||||
if x >= shadowMargin-4 && y >= shadowMargin-4 && x <= w+shadowMargin+4 && y <= h+shadowMargin+4 {
|
||||
_, _, _, a := maskImg.At(x-shadowMargin, y-shadowMargin).RGBA()
|
||||
c = color.NRGBA{R: cr8, G: cg8, B: cb8, A: uint8(a >> 8)}
|
||||
}
|
||||
backing.SetNRGBA(x, y, color.NRGBA{R: 255, G: 255, B: 255, A: 0})
|
||||
newImg.SetNRGBA(x, y+offsetY, c)
|
||||
}
|
||||
}
|
||||
|
||||
// Blur
|
||||
blurredImage := imaging.Blur(newImg, sigma)
|
||||
backing = imaging.OverlayCenter(backing, blurredImage, 0.6)
|
||||
|
||||
return backing
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ type sessionStore struct {
|
|||
baseDir string
|
||||
}
|
||||
|
||||
func (ss *sessionStore) create(newSession models.ImageEditSession) error {
|
||||
func (ss *sessionStore) save(newSession *models.ImageEditSession) error {
|
||||
sessionMeta, err := json.Marshal(newSession)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -28,21 +28,21 @@ func (ss *sessionStore) create(newSession models.ImageEditSession) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ss *sessionStore) get(guid string) (models.ImageEditSession, error) {
|
||||
func (ss *sessionStore) get(guid string) (*models.ImageEditSession, error) {
|
||||
sessionDataBts, err := os.ReadFile(filepath.Join(ss.baseDir, guid, "session.json"))
|
||||
if err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionData := models.ImageEditSession{}
|
||||
if err := json.Unmarshal(sessionDataBts, &sessionData); err != nil {
|
||||
return models.ImageEditSession{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sessionData, nil
|
||||
return &sessionData, nil
|
||||
}
|
||||
|
||||
func (ss *sessionStore) getImage(session models.ImageEditSession, imageFilename string) (string, func() (io.ReadCloser, error), error) {
|
||||
func (ss *sessionStore) getImage(session *models.ImageEditSession, imageFilename string) (string, func() (io.ReadCloser, error), error) {
|
||||
fullPath := filepath.Join(ss.baseDir, session.GUID, imageFilename)
|
||||
if s, err := os.Stat(fullPath); err != nil {
|
||||
return "", nil, err
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
Add Processor
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
<li><a class="dropdown-item" href="#" data-action="edit-upload#addProcessor">Shadow</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue