Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/style durian #4

Merged
merged 2 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ $ capybara style melon

```bash
$ capybara style pineapple
$ capybara style durian
```

<table>
Expand All @@ -88,6 +89,7 @@ $ capybara style pineapple
</tr>
<tr>
<td><img src="docs/image/style-pineapple.webp" width=270></td>
<td><img src="docs/image/style-durian.webp" width=270></td>
</tr>
</table>

Expand Down
1 change: 1 addition & 0 deletions cmd/cmds_style.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func init() {
styleCmd.AddCommand(style.TextBottomCmd)
styleCmd.AddCommand(style.LogoMelonCmd)
styleCmd.AddCommand(style.PineappleCmd)
styleCmd.AddCommand(style.DurianCmd)
}

var styleCmd = &cobra.Command{
Expand Down
49 changes: 49 additions & 0 deletions cmd/style/durian.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package style

import (
"github.com/sincerefly/capybara/base/log"
"github.com/sincerefly/capybara/cmd/border_common"
"github.com/sincerefly/capybara/cmd/cmdutils"
"github.com/sincerefly/capybara/service/style"
"github.com/spf13/cobra"
)

var DurianCmd = &cobra.Command{
Use: "durian",
Short: "Style: Gaussian blur background",
Run: func(cmd *cobra.Command, args []string) {

parameter := &style.DurianParameter{}

input := cmdutils.GetParam(cmd.Flags(), "input")
parameter.SetInput(input)

output := cmdutils.GetParam(cmd.Flags(), "output")
parameter.SetOutput(output)

// width param
width := cmdutils.GetIntParam(cmd.Flags(), "width")
if fixedWidth, fixed := border_common.FixedBorderWidth(width); fixed {
log.Warn("border width fixed with %d", fixedWidth)
width = fixedWidth
}
parameter.SetBorderWidth(width)

// with subtitle
withoutSubtitle := cmdutils.GetBoolParam(cmd.Flags(), "without-subtitle")
parameter.SetWithoutSubtitle(withoutSubtitle)

// run
log.Debugf("parameter: %s", parameter.JSONString())
style.NewStyleProcessor(style.StyleDurian, parameter).Run()
},
}

func init() {

flags := DurianCmd.Flags()
flags.StringP("input", "i", "input", "specify input folder")
flags.StringP("output", "o", "output", "specify output folder")
flags.IntP("width", "w", 500, "specify border width")
flags.BoolP("without-subtitle", "", false, "without subtitle")
}
Binary file added docs/image/style-durian.webp
Binary file not shown.
261 changes: 261 additions & 0 deletions service/style/durian.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package style

import (
"fmt"
"image"
"image/color"
"strings"

"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/sincerefly/capybara/base/log"
"github.com/sincerefly/capybara/global"
"github.com/sincerefly/capybara/resources"
"github.com/sincerefly/capybara/service/style/styles_common"
"github.com/sincerefly/capybara/structure/fileitem"
"github.com/sincerefly/capybara/structure/layout"
"github.com/sincerefly/capybara/structure/size"
"github.com/sincerefly/capybara/structure/text"
"github.com/sincerefly/capybara/utils/exif"
"github.com/sincerefly/capybara/utils/ggwrapper"
"golang.org/x/image/colornames"
)

type DurianProcessor struct {
params *DurianParameter
fiStore *fileitem.Store
}

func NewDurianProcessor(params *DurianParameter, fiStore *fileitem.Store) *DurianProcessor {
return &DurianProcessor{
params: params,
fiStore: fiStore,
}
}

func (s *DurianProcessor) Run() error {
if s.fiStore == nil {
return nil
}

// parser exif meta data
newStore, err := styles_common.SupplementaryMetaToStore(s.fiStore)
if err != nil {
return err
}

if global.ParamNoParallelism {
fileitem.LoopExecutor(newStore, s.runner)
} else {
fileitem.PoolExecutor(newStore, s.runner)
}
return nil
}

func (s *DurianProcessor) runner(fi fileitem.FileItem) error {

srcImageKey := fi.GetSourceKey()
outImageKey := fi.GetTargetKey()
meta := fi.GetExifMeta()

middleText := meta.ModelSafe()
rightText := meta.MakeSafe()

if rightText == "NIKON CORPORATION" { // shorten nikon make
rightText = "NIKON"
}

borderWidth := s.params.borderWidth

img, err := imaging.Open(srcImageKey, imaging.AutoOrientation(true))
if err != nil {
log.Fatalf("failed to open image %v", err)
}

imgSizePair := size.NewSizePair(
img.Bounds().Dx(),
img.Bounds().Dy(),
img.Bounds().Dx()+2*borderWidth,
img.Bounds().Dy(), // rewrite latter
)

// resize image for background
imgResized := imaging.Resize(img, imgSizePair.DstWidth(), 0, imaging.Lanczos)

imgSizePair.SetDstHeight(imgResized.Bounds().Dy()) // reset dst height

// new target image
dst := imaging.New(imgSizePair.DstWidth(), imgResized.Bounds().Dy(), color.RGBA{})

backgroundImg := imaging.Blur(imgResized, 80) // 50
dst = imaging.Paste(dst, backgroundImg, image.Pt(0, 0))

// paste the original image onto a new background
roundedImg := ggwrapper.ApplyRoundedCorners(img, 150) // 50 是圆角半径,可以根据需要调整

y := (imgSizePair.DstHeight() - imgSizePair.SrcHeight()) / 3
dst = imaging.Overlay(dst, roundedImg, image.Pt(borderWidth, y), 1.0)

// create draw context
dc := gg.NewContextForImage(dst)

// draw title and subtitle
titleSize, err := s.drawTitle(dc, imgSizePair, middleText, rightText)
if err != nil {
log.Fatalf("failed to draw title %v", err)
return err
}

if !s.params.WithoutSubtitle() {
subtitle := s.subtitle(meta)
err = s.drawSubtitle(dc, imgSizePair, titleSize, subtitle)
if err != nil {
log.Fatalf("failed to draw sub-title %v", err)
return err
}
}

err = imaging.Save(dc.Image(), outImageKey)
if err != nil {
log.Fatalf("failed to save image %v", err)
return err
}

log.Infof("with text_bottom saved to %s", outImageKey)
return nil
}

func (s *DurianProcessor) fixedFontSize(imgSizePair size.Pair) float64 {
fixedSize := float64((imgSizePair.DstHeight() - imgSizePair.SrcHeight()) / 6)
if fixedSize > 150 {
return 150
}
return fixedSize
}

func (s *DurianProcessor) drawTitle(dc *gg.Context, imgSizePair size.Pair, middleText, rightText string) (*size.FloatSize, error) {

const leftText = "Shot on"

// font size
fontSize := s.fixedFontSize(imgSizePair)

leftRt := text.NewRichText(
leftText,
resources.AlibabaPuHiTi3LightTTF,
fontSize,
color.White,
)
middleRt := text.NewRichText(
middleText,
resources.AlibabaPuHiTi3BoldTTF,
fontSize,
color.White,
)
rightRt := text.NewRichText(
rightText,
resources.AlibabaPuHiTi3LightTTF,
fontSize,
color.White,
)
rTexts := []text.RichText{leftRt, middleRt, rightRt}

newRTexts, textSize := s.textContainerLayout(imgSizePair, nil, rTexts)

if err := ggwrapper.DrawString(dc, newRTexts); err != nil {
return nil, err
}

return &textSize, nil
}

func (s *DurianProcessor) drawSubtitle(dc *gg.Context, imgSizePair size.Pair, titleSize *size.FloatSize, subtitle string) error {

fontSize := s.fixedFontSize(imgSizePair)

richText := text.NewRichText(
subtitle, // e.g., "70mm f/4.0 1/800s ISO250"
resources.AlibabaPuHiTi3LightTTF,
fontSize*0.8,
colornames.White,
)
rTexts := []text.RichText{richText}

offsetPadding := layout.NewPaddingTop(titleSize.Height * 1.2)

newRTexts, _ := s.textContainerLayout(imgSizePair, &offsetPadding, rTexts)

if err := ggwrapper.DrawString(dc, newRTexts); err != nil {
return err
}
return nil
}

func (s *DurianProcessor) subtitle(meta exif.Meta) string {
focalText := strings.ReplaceAll(meta.FocalLengthIn35mmFormatSafe(), " ", "")
return fmt.Sprintf("%s f/%s %ss ISO%s", focalText, meta.ApertureSafe(), meta.ShutterSpeedSafe(), meta.ISOSafe())
}

// calculate text container start x,y position
func (s *DurianProcessor) calculateBaseXY(imgSizePair size.Pair, textDim size.FloatSize) layout.Position {

baseX := float64(imgSizePair.DstWidth()/2) - textDim.Width/2
baseY := float64((imgSizePair.DstHeight()-imgSizePair.SrcHeight())/3*2+imgSizePair.SrcHeight()) - textDim.Height/2 // - s.fixedHeight()

return layout.NewPosition(baseX, baseY)
}

func (s *DurianProcessor) textContainerLayout(imgSizePair size.Pair, offsetPadding *layout.Padding,
rTexts []text.RichText) ([]text.RichText, size.FloatSize) {

const spacing = " "

var offsetPaddingLeft, offsetPaddingTop float64
if offsetPadding != nil {
offsetPaddingLeft = offsetPadding.PaddingLeft()
offsetPaddingTop = offsetPadding.PaddingTop()
}

paddings := make([]layout.Padding, 0, len(rTexts))

var paddingLeft float64
var textContainerWidth, textContainerHeight float64

// calculate text container size and padding-left info
for i, rText := range rTexts {
dc, _ := rText.Context(imgSizePair.DstWidth(), imgSizePair.DstHeight())
width, height := dc.MeasureString(rText.Text())

padding := layout.NewPadding(paddingLeft+offsetPaddingLeft, offsetPaddingTop)
paddings = append(paddings, padding)
spacingWidth, _ := dc.MeasureString(spacing)
paddingLeft += width + spacingWidth

if i != 0 {
textContainerWidth += spacingWidth
}

textContainerWidth += width
if height > textContainerHeight {
textContainerHeight = height
}
}

textContainerSize := size.FloatSize{
Width: textContainerWidth,
Height: textContainerHeight,
}

basePosition := s.calculateBaseXY(imgSizePair, textContainerSize)

// append position to rich text
newRTexts := make([]text.RichText, len(rTexts))
for i, rText := range rTexts {
x1 := basePosition.BaseX() + paddings[i].PaddingLeft()
y1 := basePosition.BaseY() + paddings[i].PaddingTop()
drawPosition := layout.NewPosition(x1, y1)
rText.SetPosition(drawPosition)
newRTexts[i] = rText
}
return newRTexts, textContainerSize
}
60 changes: 60 additions & 0 deletions service/style/durian_parameter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package style

import (
"encoding/json"
)

type DurianParameter struct {
input string
output string
borderWidth int
withoutSubTitle bool
}

func (p *DurianParameter) JSONString() string {

resp := map[string]any{
"input": p.Input(),
"output": p.Output(),
"borderWidth": p.BorderWidth(),
"withoutSubTitle": p.WithoutSubtitle(),
}

b, err := json.Marshal(resp)
if err != nil {
return ""
}
return string(b)
}

func (p *DurianParameter) Input() string {
return p.input
}

func (p *DurianParameter) SetInput(input string) {
p.input = input
}

func (p *DurianParameter) Output() string {
return p.output
}

func (p *DurianParameter) SetOutput(output string) {
p.output = output
}

func (p *DurianParameter) BorderWidth() int {
return p.borderWidth
}

func (p *DurianParameter) SetBorderWidth(borderWidth int) {
p.borderWidth = borderWidth
}

func (p *DurianParameter) WithoutSubtitle() bool {
return p.withoutSubTitle
}

func (p *DurianParameter) SetWithoutSubtitle(withoutSubTitle bool) {
p.withoutSubTitle = withoutSubTitle
}
Loading
Loading