Skip to content

Commit

Permalink
Optimize image size and display
Browse files Browse the repository at this point in the history
  • Loading branch information
qianlifeng committed Nov 10, 2023
1 parent 97e8af8 commit 3ad8c6e
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Wox.Plugin.Host.Nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"typescript": "^5.2.2"
},
"dependencies": {
"@wox-launcher/wox-plugin": "^0.0.48",
"@wox-launcher/wox-plugin": "^0.0.49",
"dayjs": "^1.11.9",
"promise-deferred": "^2.0.4",
"winston": "^3.10.0",
Expand Down
8 changes: 4 additions & 4 deletions Wox.Plugin.Host.Nodejs/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Wox.Plugin.Host.Nodejs/src/jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async function initPlugin(request: PluginJsonRpcRequest, ws: WebSocket) {
const init = getMethod(request, "init")
const pluginApi = new PluginAPI(ws, request.PluginId, request.PluginName)
pluginApiMap.set(request.PluginId, pluginApi)
return init({ API: pluginApi } as PluginInitContext)
return init({ API: pluginApi, PluginDirectory: request.Params.PluginDirectory } as PluginInitContext)
}

async function onPluginSettingChange(request: PluginJsonRpcRequest) {
Expand Down
2 changes: 1 addition & 1 deletion Wox.Plugin.Nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wox-launcher/wox-plugin",
"version": "0.0.48",
"version": "0.0.49",
"description": "All nodejs plugin for Wox should use types in this package",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions Wox.Plugin.Nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface ActionContext {

export interface PluginInitContext {
API: PublicAPI
PluginDirectory: string
}

export interface PublicAPI {
Expand Down
14 changes: 10 additions & 4 deletions Wox.UI.Tauri/src/components/WoxImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ export default (props: { img: WOXMESSAGE.WoxImage, height: number, width: number
}

const Style = styled.div<{ width: number; height: number }>`
display: flex;
height: ${props => props.height}px;
width: ${props => props.width}px;
justify-content: center;
align-items: center;
.wox-image {
line-height: ${props => props.height}px;
height: ${props => props.height}px;
width: ${props => props.width}px;
max-height: ${props => props.height}px;
max-width: ${props => props.width}px;
text-align: center;
vertical-align: middle;
svg {
width: ${props => props.height}px !important;
height: ${props => props.height}px !important;
max-width: ${props => props.height}px !important;
max-height: ${props => props.height}px !important;
}
}
`
2 changes: 1 addition & 1 deletion Wox.UI.Tauri/src/components/WoxQueryResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export default React.forwardRef((_props: WoxQueryResultProps, ref: React.Ref<Wox
event.stopPropagation()
}}>
<div className={"wox-result-image"}>
<WoxImage img={result.Icon} height={40} width={40} />
<WoxImage img={result.Icon} height={36} width={36} />
</div>
<div className={"wox-result-title-container"}>
<h2 className={"wox-result-title"}>{result.Title}</h2>
Expand Down
3 changes: 2 additions & 1 deletion Wox/plugin/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ type SystemPlugin interface {
}

type InitParams struct {
API API
API API
PluginDirectory string
}
4 changes: 3 additions & 1 deletion Wox/plugin/host/host_websocket_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func NewWebsocketPlugin(metadata plugin.Metadata, websocketHost *WebsocketHost)
}

func (w *WebsocketPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
w.websocketHost.invokeMethod(ctx, w.metadata, "init", make(map[string]string))
w.websocketHost.invokeMethod(ctx, w.metadata, "init", map[string]string{
"PluginDirectory": initParams.PluginDirectory,
})
}

func (w *WebsocketPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
Expand Down
206 changes: 196 additions & 10 deletions Wox/plugin/image.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package plugin

import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/disintegration/imaging"
"image"
"image/png"
"os"
"path"
"strings"
"wox/util"
)

var localImageMap = util.NewHashMap[string, string]()

type WoxImageType = string

var notPngErr = errors.New("image is not png")

const (
WoxImageTypeAbsolutePath = "absolute"
WoxImageTypeRelativePath = "relative"
Expand All @@ -24,6 +34,82 @@ type WoxImage struct {
ImageData string
}

func (w *WoxImage) canConvertToPng() bool {
if w.ImageType == WoxImageTypeBase64 {
return strings.HasPrefix(w.ImageData, "data:image/png;")
}
if w.ImageType == WoxImageTypeAbsolutePath {
return strings.HasSuffix(w.ImageData, ".png")
}
if w.ImageType == WoxImageTypeRelativePath {
//currently we don't support relative path image, you should convert to absolute path first
return false
}
if w.ImageType == WoxImageTypeSvg {
return true
}
if w.ImageType == WoxImageTypeUrl {
//currently we don't support url image, because we need to download it first which is not efficient
return false
}

return false
}

func (w *WoxImage) ToPng() (image.Image, error) {
if !w.canConvertToPng() {
return nil, notPngErr
}

if w.ImageType == WoxImageTypeBase64 {
data := strings.Split(w.ImageData, ",")[1]
decodedData, base64DecodeErr := base64.StdEncoding.DecodeString(data)
if base64DecodeErr != nil {
return nil, base64DecodeErr
}

imgReader := bytes.NewReader(decodedData)
return png.Decode(imgReader)
}

if w.ImageType == WoxImageTypeAbsolutePath {
imgReader, openErr := os.Open(w.ImageData)
if openErr != nil {
return nil, openErr
}
defer imgReader.Close()
return png.Decode(imgReader)
}

if w.ImageType == WoxImageTypeSvg {

}

return nil, fmt.Errorf("unsupported image type: %s", w.ImageType)
}

func (w *WoxImage) ToImage() (image.Image, error) {
if w.ImageType == WoxImageTypeAbsolutePath {
return imaging.Open(w.ImageData)
}
if w.ImageType == WoxImageTypeBase64 {
data := strings.Split(w.ImageData, ",")[1]
decodedData, base64DecodeErr := base64.StdEncoding.DecodeString(data)
if base64DecodeErr != nil {
return nil, base64DecodeErr
}

imgReader := bytes.NewReader(decodedData)
return png.Decode(imgReader)
}

return nil, fmt.Errorf("unsupported image type: %s", w.ImageType)
}

func (w *WoxImage) Hash() string {
return util.Md5([]byte(w.ImageType + w.ImageData))
}

func NewWoxImageSvg(svg string) WoxImage {
return WoxImage{
ImageType: WoxImageTypeSvg,
Expand All @@ -45,21 +131,121 @@ func NewWoxImageBase64(data string) WoxImage {
}
}

func convertLocalImageToUrl(ctx context.Context, image WoxImage, pluginInstance *Instance) (newImage WoxImage) {
func ConvertIcon(ctx context.Context, image WoxImage, pluginDirectory string) (newImage WoxImage) {
newImage = convertRelativePathToAbsolutePath(ctx, image, pluginDirectory)
newImage = cropPngTransparentPaddings(ctx, newImage)
newImage = resizeImage(ctx, newImage, 40)
newImage = convertLocalImageToUrl(ctx, newImage)
return
}

func resizeImage(ctx context.Context, image WoxImage, size int) (newImage WoxImage) {
newImage = image

if image.ImageType == WoxImageTypeAbsolutePath {
//make sure same image has the same id
id := util.Md5([]byte(pluginInstance.Metadata.Id + image.ImageType + image.ImageData))
newImage.ImageType = WoxImageTypeUrl
newImage.ImageData = fmt.Sprintf("http://localhost:%d/image?id=%s", GetPluginManager().GetUI().GetServerPort(ctx), id)
localImageMap.Store(id, image.ImageData)
imgHash := image.Hash()
resizeImgPath := path.Join(util.GetLocation().GetImageCacheDirectory(), fmt.Sprintf("resize_%d_%s.png", size, imgHash))
if _, err := os.Stat(resizeImgPath); err == nil {
return NewWoxImageAbsolutePath(resizeImgPath)
}

img, imgErr := image.ToImage()
if imgErr != nil {
return image
}

// respect ratio, remain longer side
width := size
height := size
if img.Bounds().Dx() > img.Bounds().Dy() {
height = 0
} else {
width = 0
}

start := util.GetSystemTimestamp()
resizeImg := imaging.Resize(img, width, height, imaging.Lanczos)
saveErr := imaging.Save(resizeImg, resizeImgPath)
if saveErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to save resize image: %s", saveErr.Error()))
return image
} else {
logger.Info(ctx, fmt.Sprintf("saved resize image: %s, cost %d ms", resizeImgPath, util.GetSystemTimestamp()-start))
}

return NewWoxImageAbsolutePath(resizeImgPath)
}

func cropPngTransparentPaddings(ctx context.Context, woxImage WoxImage) (newImage WoxImage) {
//try load from cache first
imgHash := woxImage.Hash()
cropImgPath := path.Join(util.GetLocation().GetImageCacheDirectory(), fmt.Sprintf("crop_padding_%s.png", imgHash))
if _, err := os.Stat(cropImgPath); err == nil {
return NewWoxImageAbsolutePath(cropImgPath)
}

pngImg, pngErr := woxImage.ToPng()
if pngErr != nil {
if !errors.Is(pngErr, notPngErr) {
logger.Error(ctx, fmt.Sprintf("failed to convert image to png: %s", pngErr.Error()))
}
return woxImage
}

start := util.GetSystemTimestamp()
bounds := pngImg.Bounds()
minX, minY, maxX, maxY := bounds.Max.X, bounds.Max.Y, bounds.Min.X, bounds.Min.Y
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
_, _, _, a := pngImg.At(x, y).RGBA()
if a != 0 {
// not transparent
if x < minX {
minX = x
}
if x > maxX {
maxX = x
}
if y < minY {
minY = y
}
if y > maxY {
maxY = y
}
}
}
}

cropImg := imaging.Crop(pngImg, image.Rect(minX, minY, maxX, maxY))
saveErr := imaging.Save(cropImg, cropImgPath)
if saveErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to save crop image: %s", saveErr.Error()))
return woxImage
} else {
logger.Info(ctx, fmt.Sprintf("saved crop image: %s, cost %d ms", cropImgPath, util.GetSystemTimestamp()-start))
}

return NewWoxImageAbsolutePath(cropImgPath)
}

func convertRelativePathToAbsolutePath(ctx context.Context, image WoxImage, pluginDirectory string) (newImage WoxImage) {
newImage = image

if image.ImageType == WoxImageTypeRelativePath {
id := util.Md5([]byte(pluginInstance.Metadata.Id + image.ImageType + image.ImageData))
newImage.ImageType = WoxImageTypeAbsolutePath
newImage.ImageData = path.Join(pluginDirectory, image.ImageData)
}

return newImage
}

func convertLocalImageToUrl(ctx context.Context, image WoxImage) (newImage WoxImage) {
newImage = image

if image.ImageType == WoxImageTypeAbsolutePath {
imgHash := image.Hash()
newImage.ImageType = WoxImageTypeUrl
newImage.ImageData = fmt.Sprintf("http://localhost:%d/image?id=%s", GetPluginManager().GetUI().GetServerPort(ctx), id)
localImageMap.Store(id, path.Join(pluginInstance.PluginDirectory, image.ImageData))
newImage.ImageData = fmt.Sprintf("http://localhost:%d/image?id=%s", GetPluginManager().GetUI().GetServerPort(ctx), imgHash)
localImageMap.Store(imgHash, image.ImageData)
}

return newImage
Expand Down
7 changes: 4 additions & 3 deletions Wox/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ func (m *Manager) initPlugin(ctx context.Context, instance *Instance) {
logger.Info(ctx, fmt.Sprintf("[%s] init plugin", instance.Metadata.Name))
instance.InitStartTimestamp = util.GetSystemTimestamp()
instance.Plugin.Init(ctx, InitParams{
API: instance.API,
API: instance.API,
PluginDirectory: instance.PluginDirectory,
})
instance.InitFinishedTimestamp = util.GetSystemTimestamp()
logger.Info(ctx, fmt.Sprintf("[%s] init plugin finished, cost %d ms", instance.Metadata.Name, instance.InitFinishedTimestamp-instance.InitStartTimestamp))
Expand Down Expand Up @@ -326,7 +327,7 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
m.resultCache.Store(result.Id, resultCache)

// convert icon
result.Icon = convertLocalImageToUrl(ctx, result.Icon, pluginInstance)
result.Icon = ConvertIcon(ctx, result.Icon, pluginInstance.PluginDirectory)
// translate title
result.Title = m.translatePlugin(ctx, pluginInstance, result.Title)
// translate subtitle
Expand Down Expand Up @@ -424,7 +425,7 @@ func (m *Manager) calculateResultScore(ctx context.Context, pluginId, title, sub

func (m *Manager) PolishRefreshableResult(ctx context.Context, pluginInstance *Instance, result RefreshableResult) RefreshableResult {
// convert icon
result.Icon = convertLocalImageToUrl(ctx, result.Icon, pluginInstance)
result.Icon = ConvertIcon(ctx, result.Icon, pluginInstance.PluginDirectory)
// translate title
result.Title = m.translatePlugin(ctx, pluginInstance, result.Title)
// translate subtitle
Expand Down
Loading

0 comments on commit 3ad8c6e

Please sign in to comment.