diff --git a/assets/js/controllers/edit_upload.js b/assets/js/controllers/edit_upload.js index 35be2f7..65b0234 100644 --- a/assets/js/controllers/edit_upload.js +++ b/assets/js/controllers/edit_upload.js @@ -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); + } + } } \ No newline at end of file diff --git a/cmds/server.go b/cmds/server.go index 5870a8a..d776cf3 100644 --- a/cmds/server.go +++ b/cmds/server.go @@ -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) diff --git a/handlers/imageedit.go b/handlers/imageedit.go index 551776a..1ca9817 100644 --- a/handlers/imageedit.go +++ b/handlers/imageedit.go @@ -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(), + }) +} diff --git a/handlers/index.go b/handlers/index.go index 6062237..410c347 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -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 diff --git a/handlers/middleware/site.go b/handlers/middleware/site.go index 6f47430..1d3ddf2 100644 --- a/handlers/middleware/site.go +++ b/handlers/middleware/site.go @@ -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) } diff --git a/models/imgedit.go b/models/imgedit.go index 88ec8be..b954402 100644 --- a/models/imgedit.go +++ b/models/imgedit.go @@ -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) diff --git a/services/imgedit/processing.go b/services/imgedit/processing.go index 83e58a9..107bcb3 100644 --- a/services/imgedit/processing.go +++ b/services/imgedit/processing.go @@ -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) } diff --git a/services/imgedit/service.go b/services/imgedit/service.go index 0b4d080..fa6b795 100644 --- a/services/imgedit/service.go +++ b/services/imgedit/service.go @@ -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) { diff --git a/services/imgedit/shadow.go b/services/imgedit/shadow.go new file mode 100644 index 0000000..4a308d0 --- /dev/null +++ b/services/imgedit/shadow.go @@ -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 +} diff --git a/services/imgedit/store.go b/services/imgedit/store.go index b697faa..7638dbe 100644 --- a/services/imgedit/store.go +++ b/services/imgedit/store.go @@ -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 diff --git a/views/uploads/edit.html b/views/uploads/edit.html index 3137e00..5c8cc2d 100644 --- a/views/uploads/edit.html +++ b/views/uploads/edit.html @@ -17,9 +17,7 @@ Add Processor