James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/*
 * Copyright 2014 Tom <tw201207@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.pages;
 
import java.nio.charset.StandardCharsets;
import java.util.List;
 
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WicketURLEncoder;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.Side;
import org.jsoup.nodes.Element;
 
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.HtmlBuilder;
 
/**
 * A {@link DiffUtils.BinaryDiffHandler BinaryDiffHandler} for images.
 *
 * @author Tom <tw201207@gmail.com>
 */
public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler {
 
    private final String oldCommitId;
    private final String newCommitId;
    private final String repositoryName;
    private final BasePage page;
    private final List<String> imageExtensions;
 
    private int imgDiffCount = 0;
 
    public ImageDiffHandler(final BasePage page, final String repositoryName, final String oldCommitId, final String newCommitId,
            final List<String> imageExtensions) {
        this.page = page;
        this.repositoryName = repositoryName;
        this.oldCommitId = oldCommitId;
        this.newCommitId = newCommitId;
        this.imageExtensions = imageExtensions;
    }
 
    /** {@inheritDoc} */
    @Override
    public String renderBinaryDiff(DiffEntry diffEntry) {
        switch (diffEntry.getChangeType()) {
        case MODIFY:
        case RENAME:
        case COPY:
            // TODO: for very small images such as icons, the slider doesn't really help. Two possible
            // approaches: either upscale them for display (may show blurry upscaled images), or show
            // them side by side (may still be too small to really make out the differences).
            String oldUrl = getImageUrl(diffEntry, Side.OLD);
            String newUrl = getImageUrl(diffEntry, Side.NEW);
            if (oldUrl != null && newUrl != null) {
                imgDiffCount++;
                String id = "imgdiff" + imgDiffCount;
                HtmlBuilder builder = new HtmlBuilder("div");
                Element wrapper = builder.root().attr("class", "imgdiff-container").attr("id", "imgdiff-" + id);
                Element container = wrapper.appendElement("div").attr("class", "imgdiff-ovr-slider").appendElement("div").attr("class", "imgdiff");
                Element old = container.appendElement("div").attr("class", "imgdiff-left");
                // style='max-width:640px;' is necessary for ensuring that the browser limits large images
                // to some reasonable width, and to override the "img { max-width: 100%; }" from bootstrap.css,
                // which would scale the left image to the width of its resizeable container, which isn't what
                // we want here. Note that the max-width must be defined directly as inline style on the element,
                // otherwise browsers ignore it if the image is larger, and we end up with an image display that
                // is too wide.
                // XXX: Maybe add a max-height, too, to limit portrait-oriented images to some reasonable height?
                // (Like a 300x10000px image...)
                old.appendElement("img").attr("class", "imgdiff-old").attr("id", id).attr("style", "max-width:640px;").attr("src", oldUrl);
                container.appendElement("img").attr("class", "imgdiff").attr("style", "max-width:640px;").attr("src", newUrl);
                wrapper.appendElement("br");
                Element controls = wrapper.appendElement("div");
                // Opacity slider
                controls.appendElement("div").attr("class", "imgdiff-opa-container").appendElement("a").attr("class", "imgdiff-opa-slider")
                        .attr("href", "#").attr("title", page.getString("gb.opacityAdjust"));
                // Blink comparator: find Pluto!
                controls.appendElement("a").attr("class", "imgdiff-link imgdiff-blink").attr("href", "#")
                        .attr("title", page.getString("gb.blinkComparator"))
                        .appendElement("img").attr("src", getStaticResourceUrl("blink32.png")).attr("width", "20");
                // Pixel subtraction, initially not displayed, will be shown by imgdiff.js depending on feature test.
                // (Uses CSS mix-blend-mode, which isn't supported on all browsers yet).
                controls.appendElement("a").attr("class", "imgdiff-link imgdiff-subtract").attr("href", "#")
                        .attr("title", page.getString("gb.imgdiffSubtract")).attr("style", "display:none;")
                        .appendElement("img").attr("src", getStaticResourceUrl("sub32.png")).attr("width", "20");
                return builder.toString();
            }
            break;
        case ADD:
            String url = getImageUrl(diffEntry, Side.NEW);
            if (url != null) {
                return new HtmlBuilder("img").root().attr("class", "diff-img").attr("src", url).toString();
            }
            break;
        default:
            break;
        }
        return null;
    }
 
    /** Returns the number of image diffs generated so far by this {@link ImageDiffHandler}. */
    public int getImgDiffCount() {
        return imgDiffCount;
    }
 
    /**
     * Constructs a URL that will fetch the designated resource in the git repository. The returned string will
     * contain the URL fully URL-escaped, but note that it may still contain unescaped ampersands, so the result
     * must still be run through HTML escaping if it is to be used in HTML.
     *
     * @return the URL to the image, if the given {@link DiffEntry} and {@link Side} refers to an image, or {@code null} otherwise.
     */
    protected String getImageUrl(DiffEntry entry, Side side) {
        String path = entry.getPath(side);
        int i = path.lastIndexOf('.');
        if (i > 0) {
            String extension = path.substring(i + 1);
            for (String ext : imageExtensions) {
                if (ext.equalsIgnoreCase(extension)) {
                    String commitId = Side.NEW.equals(side) ? newCommitId : oldCommitId;
                    if (commitId != null) {
                        return RawServlet.asLink(page.getContextUrl(), urlencode(repositoryName), commitId, urlencode(path));
                    } else {
                        return null;
                    }
                }
            }
        }
        return null;
    }
 
    /**
     * Returns a URL that will fetch the designated static resource from within GitBlit.
     */
    protected String getStaticResourceUrl(String contextRelativePath) {
        return WebApplication.get().getRequestCycleProcessor().getRequestCodingStrategy().rewriteStaticRelativeUrl(contextRelativePath);
    }
 
    /**
     * Encode a URL component of a {@link RawServlet} URL in the special way that the servlet expects it. Note that
     * the %-encoding used does not encode '&amp;' or '&lt;'. Slashes are not encoded in the result.
     *
     * @param component
     *            to encode using %-encoding
     * @return the encoded component
     */
    protected String urlencode(final String component) {
        // RawServlet handles slashes itself. Note that only the PATH_INSTANCE fits the bill here: it encodes
        // spaces as %20, and we just have to correct for encoded slashes. Java's standard URLEncoder would
        // encode spaces as '+', and I don't know what effects that would have on other parts of GitBlit. It
        // would also be wrong for path components (but fine for a query part), so we'd have to correct it, too.
        //
        // Actually, this should be done in RawServlet.asLink(). As it is now, this may be incorrect if that
        // operation ever uses query parameters instead of paths, or if it is fixed to urlencode its path
        // components. But I don't want to touch that static method in RawServlet.
        return WicketURLEncoder.PATH_INSTANCE.encode(component, StandardCharsets.UTF_8.name()).replaceAll("%2[fF]", "/");
    }
}