diff --git a/config/tree.go b/config/tree.go index 037645d..6934221 100644 --- a/config/tree.go +++ b/config/tree.go @@ -46,6 +46,18 @@ type MarkdownConfig struct { ChromaStyle *string `yaml:"ChromaStyle"` } +// ImagingConfig defines parameter for imaging processing +type ImagingConfig struct { + Width int `yaml:"Width"` + Height int `yaml:"Height"` + Process string `yaml:"Process"` + Anchor string `yaml:"Anchor"` + Quality int `yaml:"Quality"` + + Filename string `yaml:"-"` + Format string `yaml:"-"` +} + // PathConfig of subdir type PathConfig struct { This ThisPathConfig `yaml:"This"` @@ -55,6 +67,7 @@ type PathConfig struct { Path *DirnameConfig `yaml:"Path"` Filename *FilenameConfig `yaml:"Filename"` Markdown *MarkdownConfig `yaml:"Markdown"` + Imaging *ImagingConfig `yaml:"Imaging"` Data map[string]interface{} `yaml:"Data"` } diff --git a/helper/template_filters.go b/helper/template_filters.go index 23eaa16..ace654b 100644 --- a/helper/template_filters.go +++ b/helper/template_filters.go @@ -1,13 +1,19 @@ package helper import ( + "crypto/md5" "errors" "fmt" + "image" "io/ioutil" + "net/http" + "net/url" + "os" "path" "strconv" "strings" + "gitbase.de/apairon/mark2web/config" "github.com/ddliu/motto" "github.com/disintegration/imaging" "github.com/flosch/pongo2" @@ -22,7 +28,7 @@ func init() { } newFilters := map[string]pongo2.FilterFunction{ - "image_resize": ImageResizeFilter, + "image_process": ImageProcessFilter, "relative_path": RelativePathFilter, } for name, fn := range newFilters { @@ -44,17 +50,15 @@ func MarkdownFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pong nil } -type imageParams struct { - width int - height int - filename string -} - -func parseImageParams(str string) (*imageParams, error) { +func parseImageParams(str string) (*config.ImagingConfig, error) { + p := config.ImagingConfig{} if str == "" { - return nil, errors.New("missing image parameters") + config.Merge(&p, currentPathConfig.Imaging) + // Filename and Format are only valid for current image + p.Filename = "" + p.Format = "" + return &p, nil } - p := imageParams{} for _, s := range strings.Split(str, ",") { e := strings.Split(s, "=") if len(e) < 2 { @@ -63,11 +67,20 @@ func parseImageParams(str string) (*imageParams, error) { var err error switch e[0] { case "w": - p.width, err = strconv.Atoi(e[1]) + p.Width, err = strconv.Atoi(e[1]) case "h": - p.height, err = strconv.Atoi(e[1]) + p.Height, err = strconv.Atoi(e[1]) case "f": - p.filename = e[1] + p.Filename = e[1] + case "p": + p.Process = e[1] + case "a": + p.Anchor = e[1] + case "q": + p.Quality, err = strconv.Atoi(e[1]) + if p.Quality < 0 || p.Quality > 100 { + err = errors.New("q= must be between 1 and 100") + } default: return nil, fmt.Errorf("invalid image parameter: %s", s) } @@ -78,9 +91,23 @@ func parseImageParams(str string) (*imageParams, error) { return &p, nil } -// ImageResizeFilter read the image url and resize parameters and saves the resized image +func getImageFromURL(url string) (image.Image, string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, "", fmt.Errorf("could not get url '%s': %s", url, err) + } + + img, format, err := image.Decode(resp.Body) + if err != nil { + return nil, "", fmt.Errorf("could read body from url '%s': %s", url, err) + } + + return img, format, nil +} + +// ImageProcessFilter read the image url and process parameters and saves the resized/processed image // param: w=WITDH,h=HEIGHT -func ImageResizeFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { +func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { imgSource := in.String() p, err := parseImageParams(param.String()) if err != nil { @@ -89,32 +116,124 @@ func ImageResizeFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *p OrigError: err, } } - - imgSource = ResolveInputPath(imgSource) - if p.filename == "" { - p.filename = fmt.Sprintf("%dx%d_%s", p.width, p.height, path.Base(imgSource)) + if p == nil { + return nil, &pongo2.Error{ + Sender: "filter:image_resize", + OrigError: errors.New("no imaging config defined"), + } } - imgTarget := ResolveOutputPath(p.filename) - Log.Noticef("resizing image from %s to %s", imgSource, imgTarget) - img, err := imaging.Open(imgSource, imaging.AutoOrientation(true)) + var img image.Image + if p.Process == "" { + p.Process = "resize" + } + filePrefix := fmt.Sprintf( + "%s_%dx%d_q%03d", + p.Process, + p.Width, + p.Height, + p.Quality, + ) + if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") { + // remote file + img, p.Format, err = getImageFromURL(imgSource) + // build filename + if p.Filename == "" { + var fBase string + if u, _ := url.Parse(imgSource); u != nil { + fBase = strings.Split(path.Base(u.Path), ".")[0] + } + + p.Filename = fmt.Sprintf( + "%s_%x_%s.%s", + filePrefix, + md5.Sum([]byte(imgSource)), + fBase, + p.Format, + ) + } + } else { + // local file + imgSource = ResolveInputPath(imgSource) + img, err = imaging.Open(imgSource, imaging.AutoOrientation(true)) + if p.Filename == "" { + p.Filename = fmt.Sprintf( + "%s_%s", + filePrefix, + path.Base(imgSource), + ) + } + } if err != nil { return nil, &pongo2.Error{ Sender: "filter:image_resize", OrigError: fmt.Errorf("could not open image '%s': %s", imgSource, err), } } - img = imaging.Resize(img, p.width, p.height, imaging.Lanczos) - err = imaging.Save(img, imgTarget) - if err != nil { - return nil, &pongo2.Error{ - Sender: "filter:image_resize", - OrigError: fmt.Errorf("could save image '%s': %s", imgTarget, err), + imgTarget := ResolveOutputPath(p.Filename) + + if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() { + Log.Noticef("skipped processing image from %s to %s, file already exists", imgSource, imgTarget) + } else { + Log.Noticef("processing image from %s to %s", imgSource, imgTarget) + + switch p.Process { + case "resize": + img = imaging.Resize(img, p.Width, p.Height, imaging.Lanczos) + case "fit": + img = imaging.Fit(img, p.Width, p.Height, imaging.Lanczos) + case "fill": + var anchor imaging.Anchor + switch strings.ToLower(p.Anchor) { + case "": + fallthrough + case "center": + anchor = imaging.Center + case "topleft": + anchor = imaging.TopLeft + case "top": + anchor = imaging.Top + case "topright": + anchor = imaging.TopRight + case "left": + anchor = imaging.Left + case "right": + anchor = imaging.Right + case "bottomleft": + anchor = imaging.BottomLeft + case "bottom": + anchor = imaging.Bottom + case "bottomright": + anchor = imaging.BottomRight + default: + return nil, &pongo2.Error{ + Sender: "filter:image_resize", + OrigError: fmt.Errorf("unknown anchor a=%s definition", p.Anchor), + } + } + img = imaging.Fill(img, p.Width, p.Height, anchor, imaging.Lanczos) + default: + return nil, &pongo2.Error{ + Sender: "filter:image_resize", + OrigError: fmt.Errorf("invalid p parameter '%s'", p.Process), + } + } + + var encodeOptions = make([]imaging.EncodeOption, 0) + if p.Quality > 0 { + encodeOptions = append(encodeOptions, imaging.JPEGQuality(p.Quality)) + } + + err = imaging.Save(img, imgTarget, encodeOptions...) + if err != nil { + return nil, &pongo2.Error{ + Sender: "filter:image_resize", + OrigError: fmt.Errorf("could save image '%s': %s", imgTarget, err), + } } } - - return pongo2.AsValue(ResolveNavPath(p.filename)), nil + return pongo2.AsValue(ResolveNavPath(p.Filename)), nil } // RelativePathFilter returns the relative path to navpoint based on current nav diff --git a/main.go b/main.go index 0b1c962..71784b0 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,12 @@ func main() { Ignore: &defaultFilenameIgnore, OutputExtension: &defaultFilenameOutputExtension, } + defaultPathConfig.Imaging = &config.ImagingConfig{ + Width: 1920, + Height: 1920, + Process: "fit", + Quality: 75, + } helper.ReadContentDir(*inDir+"/content", *outDir, "", defaultPathConfig, contentConfig) //spew.Dump(contentConfig) diff --git a/test.rest b/test.rest index 39fd120..cc166e6 100644 --- a/test.rest +++ b/test.rest @@ -1 +1,105 @@ -GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog?filter[published]=true&sort[date]=-1&limit=1&skip=0 \ No newline at end of file +GET https://mark2web.basiscms.de/api/collections/get/mark2webBlog + ?sort[date]=-1 + &limit=1 + &token=89ff216524093123bf7a0a10f7b273 + +### + +POST https://ci.basehosts.de/hook?secret=PaqXhk1R1imq67wpNQL2BAHuNUJj9kEd +Content-Type: application/json +X-GitHub-Delivery: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +X-GitHub-Event: push +X-Gitea-Delivery: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +X-Gitea-Event: push +X-Gogs-Delivery: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +X-Gogs-Event: push + +{ + "secret": "PaqXhk1R1imq67wpNQL2BAHuNUJj9kEd", + "ref": "refs/heads/master", + "before": "HEAD", + "after": "HEAD", + "commits": [ + { + "id": "8e849014654a4d1c6d78a9aa69e1c64df2e043df", + "message": "fixed run, if templates/filters is missing\n", + "url": "https://gitbase.de/apairon/mark2web/commit/8e849014654a4d1c6d78a9aa69e1c64df2e043df", + "author": { + "name": "Sebastian Frank", + "email": "frank@basiskonfiguration.de", + "username": "" + }, + "committer": { + "name": "Sebastian Frank", + "email": "frank@basiskonfiguration.de", + "username": "" + }, + "verification": null, + "timestamp": "0001-01-01T00:00:00Z" + } + ], + "repository": { + "id": 59, + "owner": { + "id": 1, + "login": "apairon", + "full_name": "Sebastian Frank", + "email": "frank@basiskonfiguration.de", + "avatar_url": "https://gitbase.de/avatars/a65ee0f49a3861e31212c474d625a073", + "language": "de-DE", + "username": "apairon" + }, + "name": "mark2web", + "full_name": "apairon/mark2web", + "description": "mark2web Website-Generator", + "empty": false, + "private": false, + "fork": false, + "parent": null, + "mirror": false, + "size": 3743, + "html_url": "https://gitbase.de/apairon/mark2web", + "ssh_url": "ssh://git@gitbase.de:2222/apairon/mark2web.git", + "clone_url": "https://gitbase.de/apairon/mark2web.git", + "website": "https://www.mark2web.de", + "stars_count": 1, + "forks_count": 0, + "watchers_count": 1, + "open_issues_count": 7, + "default_branch": "master", + "archived": false, + "created_at": "2019-02-10T11:40:58+01:00", + "updated_at": "2019-02-28T18:41:26+01:00", + "permissions": { + "admin": false, + "push": false, + "pull": false + } + }, + "pusher": { + "id": 1, + "login": "apairon", + "full_name": "Sebastian Frank", + "email": "frank@basiskonfiguration.de", + "avatar_url": "https://gitbase.de/avatars/a65ee0f49a3861e31212c474d625a073", + "language": "de-DE", + "username": "apairon" + }, + "sender": { + "id": 1, + "login": "apairon", + "full_name": "Sebastian Frank", + "email": "frank@basiskonfiguration.de", + "avatar_url": "https://gitbase.de/avatars/a65ee0f49a3861e31212c474d625a073", + "language": "de-DE", + "username": "apairon" + } +} + +### + +GET https://mark2web.basiscms.de/api/imagestyles/style/klein?src=/vhosts/mark2web.basiscms.de/uploads/2019/02/27/5c767a7f3dec9computer-3368242_1920.jpg&output=1&token=89ff216524093123bf7a0a10f7b273 + +### + +GET https://mark2web.basiscms.de/api/imagestyles/style diff --git a/website/content/config.yml b/website/content/config.yml index 0d04139..bc9fc43 100644 --- a/website/content/config.yml +++ b/website/content/config.yml @@ -11,4 +11,5 @@ Markdown: ChromaStyle: monokai Data: - matomoSiteId: 89 \ No newline at end of file + matomoSiteId: 89 + token: 89ff216524093123bf7a0a10f7b273 # cockpit api token \ No newline at end of file diff --git a/website/templates/base.html b/website/templates/base.html index d74134b..708036d 100755 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -85,6 +85,8 @@ {% block part1 %} {{ BodyParts.1 }} {% endblock part1 %} + {% if This.Data.img %} + {% endif %} diff --git a/website/templates/base_blog.html b/website/templates/base_blog.html index e58a776..3ce9c30 100644 --- a/website/templates/base_blog.html +++ b/website/templates/base_blog.html @@ -2,7 +2,7 @@ {% block part0 %} {{ Body }} - {% for e in fnRequest("https://mark2web.basiscms.de/api/collections/get/mark2webBlog?filter[published]=true&sort[date]=-1&limit=1").entries %} + {% for e in fnRequest("https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token="|add:Data.token|add:"&filter[published]=true&sort[date]=-1&limit=1").entries %}

{{ e.title }}
{{ e.date|datum }}
@@ -19,7 +19,7 @@ {% comment %} limit wird für skip in Query gebraucht {% endcomment %} - {% for e in fnRequest("https://mark2web.basiscms.de/api/collections/get/mark2webBlog?filter[published]=true&sort[date]=-1&skip=1&limit=100").entries %} + {% for e in fnRequest("https://mark2web.basiscms.de/api/collections/get/mark2webBlog?token="|add:Data.token|add:"&filter[published]=true&sort[date]=-1&skip=1&limit=100").entries %}

{{ e.title }}
{{ e.date|datum }}
diff --git a/website/templates/base_blog_details.html b/website/templates/base_blog_details.html index 45916d8..af771bd 100644 --- a/website/templates/base_blog_details.html +++ b/website/templates/base_blog_details.html @@ -10,6 +10,7 @@ {% block part1 %} {{ Body }} - +
+ « zurück {% endblock part1 %}