jekyll-image-links 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d2fc33af0a1cda25f029a7a72d68e32eefa8f1fa412fdd4f69281fdb8676a29
|
|
4
|
+
data.tar.gz: 174005c6a08c2f617d953ebe2d874f324c3bfd0107436e93ff23f490bc623606
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cab285f59d4c32b302686e4995113c2b8ceaba8bbeaf29d9ec2a329ffd2ded5b8de487f6b26f62049c25c9fd155a7d64665d5cd5f807966d6aefae15618670f
|
|
7
|
+
data.tar.gz: 3be18ab75e132c7c7ad4b054879fac3ebb27d72efaedc7b34be916765b049b4c27cc8c45648f3c819ec8bd55fa1df6fe06895adfb380cec51087c14fd74ca526
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
const ROOT_SELECTOR = '[data-jil-root="true"]';
|
|
5
5
|
const config = window.__JIL_CONFIG__ || {};
|
|
6
|
+
// Hosts that already have listeners on this element instance.
|
|
7
|
+
// data-jil-ready is not enough: a hover-popup preview clones the live map,
|
|
8
|
+
// so the attribute is copied, but click listeners are not. A WeakSet tracks
|
|
9
|
+
// the actual element. Clones are absent from it and get set up again, and
|
|
10
|
+
// entries disappear when the preview window is discarded.
|
|
11
|
+
const readyHosts = new WeakSet();
|
|
6
12
|
|
|
7
13
|
function getHoverPopup() {
|
|
8
14
|
const api = window.JekyllHoverPopup;
|
|
@@ -17,42 +23,225 @@
|
|
|
17
23
|
return !!getHoverPopup();
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function logError(message, error) {
|
|
27
|
+
if (error) console.error(`[jil] ${message}`, error);
|
|
28
|
+
else console.error(`[jil] ${message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function reportMapProblems(img, mapData) {
|
|
32
|
+
const src = mapData.src || img.getAttribute("src") || "image map";
|
|
33
|
+
const width = Number(mapData.width);
|
|
34
|
+
const height = Number(mapData.height);
|
|
35
|
+
|
|
36
|
+
if (!width || !height) {
|
|
37
|
+
logError(`${src}: map data is missing width or height`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const regions = mapData.regions;
|
|
41
|
+
if (!Array.isArray(regions) || regions.length === 0) {
|
|
42
|
+
logError(`${src}: map data has no regions`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const checkSize = () => {
|
|
46
|
+
const naturalWidth = img.naturalWidth;
|
|
47
|
+
const naturalHeight = img.naturalHeight;
|
|
48
|
+
if (!naturalWidth || !naturalHeight) {
|
|
49
|
+
logError(`${src}: image failed to load`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!width || !height) return;
|
|
53
|
+
if (naturalWidth === width && naturalHeight === height) return;
|
|
54
|
+
logError(`${src}: image is ${naturalWidth}×${naturalHeight} but map data is ${width}×${height}`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
img.addEventListener("error", () => {
|
|
58
|
+
logError(`${src}: image failed to load`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (img.complete) checkSize();
|
|
62
|
+
else img.addEventListener("load", checkSize);
|
|
63
|
+
|
|
64
|
+
(regions || []).forEach((region, index) => {
|
|
65
|
+
const name = region.title || region.href || `region ${index + 1}`;
|
|
66
|
+
const points = region.points;
|
|
67
|
+
if (!region.href) logError(`${src}: "${name}" is missing href`);
|
|
68
|
+
if (!Array.isArray(points) || points.length < 3) {
|
|
69
|
+
logError(`${src}: "${name}" needs at least 3 points`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!width || !height) return;
|
|
73
|
+
|
|
74
|
+
const xs = points.map((point) => point[0]);
|
|
75
|
+
const ys = points.map((point) => point[1]);
|
|
76
|
+
const outside = xs.some((x) => x < 0 || x > width) || ys.some((y) => y < 0 || y > height);
|
|
77
|
+
if (!outside) return;
|
|
78
|
+
logError(
|
|
79
|
+
`${src}: "${name}" is outside ${width}×${height} (x ${Math.min(...xs)}–${Math.max(...xs)}, y ${Math.min(...ys)}–${Math.max(...ys)})`
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
20
84
|
function init() {
|
|
21
|
-
document.querySelectorAll("[data-jil-
|
|
85
|
+
document.querySelectorAll("img.jil-map-image[data-jil-regions]").forEach((img) => {
|
|
86
|
+
if (img.closest("[data-jil-map]")) return;
|
|
87
|
+
logError(
|
|
88
|
+
`${img.getAttribute("src") || "image"} was not enhanced (${img.getAttribute("data-jil-regions")})`
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Maps already in the page. Maps that appear later, such as inside a
|
|
93
|
+
// preview window, are handled by watchInsertedMaps.
|
|
94
|
+
initWithin(document);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set up every map host inside root. root may be the document (first load)
|
|
98
|
+
// or a node just inserted into a preview window.
|
|
99
|
+
function initWithin(root) {
|
|
100
|
+
if (!root || !root.querySelectorAll) return;
|
|
101
|
+
|
|
102
|
+
const hosts = [];
|
|
103
|
+
// The inserted node can be the host itself. querySelectorAll only looks
|
|
104
|
+
// at descendants, so include root when it is a map host.
|
|
105
|
+
if (root.nodeType === 1 && root.matches("[data-jil-map]")) hosts.push(root);
|
|
106
|
+
root.querySelectorAll("[data-jil-map]").forEach((host) => hosts.push(host));
|
|
107
|
+
|
|
108
|
+
hosts.forEach((host) => {
|
|
109
|
+
try {
|
|
110
|
+
initMapHost(host);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logError(`Failed to init ${host.id || "image map"}`, error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Page load only sees maps in the original document. Preview windows fetch
|
|
118
|
+
// or clone section HTML later, so watch for those nodes and set them up then.
|
|
119
|
+
function watchInsertedMaps() {
|
|
120
|
+
if (!document.body || typeof MutationObserver === "undefined") return;
|
|
121
|
+
|
|
122
|
+
const observer = new MutationObserver((mutations) => {
|
|
123
|
+
mutations.forEach((mutation) => {
|
|
124
|
+
mutation.addedNodes.forEach((node) => {
|
|
125
|
+
// Text nodes cannot contain a map host.
|
|
126
|
+
if (node.nodeType !== 1) return;
|
|
127
|
+
initWithin(node);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// subtree covers maps nested inside the preview's section wrapper,
|
|
133
|
+
// not only nodes appended directly to body.
|
|
134
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Drop UI that was copied from an already-initialized map. Labels and the
|
|
138
|
+
// viewer button are created in JS, so a clone includes the elements but not
|
|
139
|
+
// their listeners. Removing them lets init add a fresh, working copy.
|
|
140
|
+
// Server-rendered markup has neither, so this is a no-op on first setup.
|
|
141
|
+
function stripGeneratedUi(host) {
|
|
142
|
+
host.querySelectorAll(".jil-label-layer").forEach((node) => node.remove());
|
|
143
|
+
const figure = host.closest(".jil-figure");
|
|
144
|
+
if (!figure) return;
|
|
145
|
+
// The toolbar is a sibling of the host, inserted after the caption.
|
|
146
|
+
figure.querySelectorAll(":scope > .jil-toolbar").forEach((node) => node.remove());
|
|
22
147
|
}
|
|
23
148
|
|
|
24
149
|
function initMapHost(host) {
|
|
25
|
-
|
|
150
|
+
// Skip hosts already bound in this document. Clones are different elements
|
|
151
|
+
// and fall through even when they carry data-jil-ready from the original.
|
|
152
|
+
if (readyHosts.has(host)) return;
|
|
153
|
+
readyHosts.add(host);
|
|
26
154
|
host.dataset.jilReady = "true";
|
|
155
|
+
stripGeneratedUi(host);
|
|
156
|
+
|
|
157
|
+
let mapData;
|
|
158
|
+
try {
|
|
159
|
+
mapData = JSON.parse(host.dataset.jilMap);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logError(`Invalid map JSON on ${host.id || "image map"}`, error);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
27
164
|
|
|
28
|
-
const mapData = JSON.parse(host.dataset.jilMap);
|
|
29
165
|
const inline = host.dataset.jilInline !== "false";
|
|
30
166
|
const viewer = host.dataset.jilViewer !== "false";
|
|
31
167
|
const labels = host.dataset.jilLabels === "true";
|
|
32
168
|
const img = host.querySelector(".jil-map-image");
|
|
33
169
|
|
|
34
|
-
if (!img)
|
|
170
|
+
if (!img) {
|
|
171
|
+
logError(`${mapData.src || host.id || "image map"}: missing .jil-map-image`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
35
174
|
|
|
175
|
+
reportMapProblems(img, mapData);
|
|
36
176
|
if (labels) renderLabels(host, mapData, img);
|
|
37
177
|
if (inline) bindInlineClicks(host, mapData, img);
|
|
38
178
|
if (viewer) addViewerButton(host, mapData);
|
|
39
179
|
}
|
|
40
180
|
|
|
181
|
+
// Keep overlay geometry in sync with the image, including inside a preview
|
|
182
|
+
// that is opened, resized, and then closed.
|
|
183
|
+
function bindLiveUpdates(host, img, update) {
|
|
184
|
+
const onLoad = () => update();
|
|
185
|
+
img.addEventListener("load", onLoad);
|
|
186
|
+
|
|
187
|
+
// Window listeners and a ResizeObserver would keep a closed preview's
|
|
188
|
+
// nodes alive. Drop them once the host has left the document.
|
|
189
|
+
const detach = () => {
|
|
190
|
+
window.removeEventListener("resize", onResize);
|
|
191
|
+
if (observer) observer.disconnect();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onResize = () => {
|
|
195
|
+
if (!host.isConnected) {
|
|
196
|
+
detach();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
update();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let observer = null;
|
|
203
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
204
|
+
observer = new ResizeObserver(() => {
|
|
205
|
+
// Closing a preview disconnects the host and fires this observer.
|
|
206
|
+
if (!host.isConnected) {
|
|
207
|
+
detach();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
update();
|
|
211
|
+
});
|
|
212
|
+
// Watch both: the host size follows the preview window, and the image
|
|
213
|
+
// size follows its own load and CSS.
|
|
214
|
+
observer.observe(host);
|
|
215
|
+
observer.observe(img);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
window.addEventListener("resize", onResize);
|
|
219
|
+
// A cached image may already be complete, so the load event never fires.
|
|
220
|
+
if (img.complete) update();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function bindOverlayGeometry(overlay, host, mapData, img) {
|
|
224
|
+
bindLiveUpdates(host, img, () => {
|
|
225
|
+
const geom = getMapGeometry(img, mapData);
|
|
226
|
+
if (!geom || !geom.width || !geom.height) return;
|
|
227
|
+
overlay.style.left = `${geom.offsetX}px`;
|
|
228
|
+
overlay.style.top = `${geom.offsetY}px`;
|
|
229
|
+
overlay.style.width = `${geom.width}px`;
|
|
230
|
+
overlay.style.height = `${geom.height}px`;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
41
234
|
function renderLabels(host, mapData, img) {
|
|
42
235
|
const overlay = document.createElement("div");
|
|
43
236
|
overlay.className = "jil-label-layer";
|
|
44
237
|
host.appendChild(overlay);
|
|
238
|
+
bindOverlayGeometry(overlay, host, mapData, img);
|
|
45
239
|
|
|
46
|
-
|
|
240
|
+
bindLiveUpdates(host, img, () => {
|
|
47
241
|
overlay.innerHTML = "";
|
|
48
242
|
const geom = getMapGeometry(img, mapData);
|
|
49
243
|
if (!geom || !geom.width || !geom.height) return;
|
|
50
244
|
|
|
51
|
-
overlay.style.left = `${geom.offsetX}px`;
|
|
52
|
-
overlay.style.top = `${geom.offsetY}px`;
|
|
53
|
-
overlay.style.width = `${geom.width}px`;
|
|
54
|
-
overlay.style.height = `${geom.height}px`;
|
|
55
|
-
|
|
56
245
|
mapData.regions.forEach((region) => {
|
|
57
246
|
const center = polygonCenter(region.points);
|
|
58
247
|
if (!center) return;
|
|
@@ -71,16 +260,7 @@
|
|
|
71
260
|
link.title = region.title || label;
|
|
72
261
|
overlay.appendChild(link);
|
|
73
262
|
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
if (img.complete) update();
|
|
77
|
-
img.addEventListener("load", update);
|
|
78
|
-
window.addEventListener("resize", update);
|
|
79
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
80
|
-
const observer = new ResizeObserver(update);
|
|
81
|
-
observer.observe(host);
|
|
82
|
-
observer.observe(img);
|
|
83
|
-
}
|
|
263
|
+
});
|
|
84
264
|
}
|
|
85
265
|
|
|
86
266
|
function bindInlineClicks(host, mapData, img) {
|
|
@@ -329,6 +509,9 @@
|
|
|
329
509
|
state.image = image;
|
|
330
510
|
fitZoom();
|
|
331
511
|
};
|
|
512
|
+
image.onerror = () => {
|
|
513
|
+
logError(`${mapData.src || "viewer image"}: viewer image failed to load`);
|
|
514
|
+
};
|
|
332
515
|
image.src = mapData.src;
|
|
333
516
|
|
|
334
517
|
return {
|
|
@@ -354,13 +537,22 @@
|
|
|
354
537
|
const jhp = useHoverPopup() ? getHoverPopup() : null;
|
|
355
538
|
if (jhp) {
|
|
356
539
|
event.preventDefault();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
540
|
+
try {
|
|
541
|
+
await jhp.openLink({
|
|
542
|
+
href: region.href,
|
|
543
|
+
title: region.title || region.label || region.href,
|
|
544
|
+
clientX: event.clientX,
|
|
545
|
+
clientY: event.clientY,
|
|
546
|
+
isPermanent: true,
|
|
547
|
+
});
|
|
548
|
+
} catch (error) {
|
|
549
|
+
logError(`Failed to open ${region.href}`, error);
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!region.href) {
|
|
555
|
+
logError("Region is missing href");
|
|
364
556
|
return;
|
|
365
557
|
}
|
|
366
558
|
|
|
@@ -478,9 +670,18 @@
|
|
|
478
670
|
}
|
|
479
671
|
|
|
480
672
|
function start() {
|
|
481
|
-
if (!document.querySelector(ROOT_SELECTOR))
|
|
673
|
+
if (!document.querySelector(ROOT_SELECTOR)) {
|
|
674
|
+
logError("image_links.js loaded without data-jil-root");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
482
677
|
|
|
483
|
-
const run = () =>
|
|
678
|
+
const run = () => {
|
|
679
|
+
// Observe before the first pass so a preview inserted during init is
|
|
680
|
+
// not missed. Init itself does not add map hosts, so this does not
|
|
681
|
+
// set the same map up twice.
|
|
682
|
+
watchInsertedMaps();
|
|
683
|
+
init();
|
|
684
|
+
};
|
|
484
685
|
|
|
485
686
|
if (document.querySelector('[data-hover-popup-root="true"]') && !getHoverPopup()) {
|
|
486
687
|
window.setTimeout(run, 0);
|
|
@@ -17,7 +17,10 @@ module Jekyll
|
|
|
17
17
|
html = MapRenderer.enhance_html(html, site: site, page: doc, cfg: cfg)
|
|
18
18
|
doc.output = inject_assets(html, assets_path: assets_path, site: site, cfg: cfg)
|
|
19
19
|
rescue StandardError => e
|
|
20
|
-
Jekyll.logger.
|
|
20
|
+
Jekyll.logger.error(
|
|
21
|
+
"jekyll-image-links:",
|
|
22
|
+
"Failed to process #{doc.relative_path}: #{e.class}: #{e.message}"
|
|
23
|
+
)
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -23,7 +23,7 @@ module Jekyll
|
|
|
23
23
|
html = html.gsub(PORTABLE_FIGURE) do |figure_html|
|
|
24
24
|
enhance_portable_markup(figure_html, site: site, page: page, cfg: cfg)
|
|
25
25
|
rescue StandardError => e
|
|
26
|
-
|
|
26
|
+
log_error("#{page_label(page)} #{markup_context(figure_html)}: #{e.class}: #{e.message}")
|
|
27
27
|
figure_html
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ module Jekyll
|
|
|
32
32
|
|
|
33
33
|
enhance_portable_markup(image_html, site: site, page: page, cfg: cfg)
|
|
34
34
|
rescue StandardError => e
|
|
35
|
-
|
|
35
|
+
log_error("#{page_label(page)} #{markup_context(image_html)}: #{e.class}: #{e.message}")
|
|
36
36
|
image_html
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -170,6 +170,8 @@ module Jekyll
|
|
|
170
170
|
"title" => title,
|
|
171
171
|
"regions" => normalize_regions(regions, site: site, page: page, liquid_context: liquid_context),
|
|
172
172
|
}
|
|
173
|
+
warn_if_regions_out_of_bounds!(map_data["regions"], width, height, src)
|
|
174
|
+
warn_if_image_dimensions_differ!(site, page, attrs["src"], width, height)
|
|
173
175
|
|
|
174
176
|
render_interactive(
|
|
175
177
|
map_data,
|
|
@@ -300,6 +302,170 @@ module Jekyll
|
|
|
300
302
|
end
|
|
301
303
|
end
|
|
302
304
|
|
|
305
|
+
def warn_if_regions_out_of_bounds!(regions, width, height, src)
|
|
306
|
+
regions.each do |region|
|
|
307
|
+
points = region["points"] || []
|
|
308
|
+
next if points.empty?
|
|
309
|
+
|
|
310
|
+
xs = points.map { |point| point[0] }
|
|
311
|
+
ys = points.map { |point| point[1] }
|
|
312
|
+
outside = xs.any? { |x| x.negative? || x > width } || ys.any? { |y| y.negative? || y > height }
|
|
313
|
+
next unless outside
|
|
314
|
+
|
|
315
|
+
name = region["title"] || region["href"]
|
|
316
|
+
log_error(
|
|
317
|
+
"#{src}: region \"#{name}\" is outside #{width}×#{height} (x #{xs.min}–#{xs.max}, y #{ys.min}–#{ys.max})"
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def warn_if_image_dimensions_differ!(site, page, src, width, height)
|
|
323
|
+
path = local_image_path(site, page, src)
|
|
324
|
+
if path.nil?
|
|
325
|
+
log_error("#{src}: image file not found") unless remote_url?(src)
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
actual = read_image_size(path)
|
|
330
|
+
return unless actual
|
|
331
|
+
|
|
332
|
+
actual_width, actual_height = actual
|
|
333
|
+
return if actual_width == width && actual_height == height
|
|
334
|
+
|
|
335
|
+
log_error("#{src}: image file is #{actual_width}×#{actual_height} but map data is #{width}×#{height}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def local_image_path(site, page, src)
|
|
339
|
+
return nil if src.to_s.strip.empty? || remote_url?(src)
|
|
340
|
+
|
|
341
|
+
path = src.to_s
|
|
342
|
+
baseurl = site.baseurl.to_s
|
|
343
|
+
baseurl = "" if baseurl == "/"
|
|
344
|
+
path = path.delete_prefix(baseurl) if !baseurl.empty? && (path == baseurl || path.start_with?("#{baseurl}/"))
|
|
345
|
+
|
|
346
|
+
candidates = [path]
|
|
347
|
+
if !path.start_with?("/") && page&.respond_to?(:relative_path) && page.relative_path
|
|
348
|
+
candidates << File.join(File.dirname(page.relative_path), path)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
candidates.each do |candidate|
|
|
352
|
+
full_path = Jekyll.sanitized_path(site.source, candidate)
|
|
353
|
+
return full_path if File.file?(full_path)
|
|
354
|
+
end
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def remote_url?(src)
|
|
359
|
+
src.to_s.start_with?("http://", "https://", "data:")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def read_image_size(path)
|
|
363
|
+
File.open(path, "rb") do |file|
|
|
364
|
+
header = file.read(32)
|
|
365
|
+
return nil unless header && header.bytesize >= 24
|
|
366
|
+
|
|
367
|
+
header = header.b
|
|
368
|
+
if header.start_with?("\x89PNG\r\n\x1A\n".b)
|
|
369
|
+
return [header[16, 4].unpack1("N"), header[20, 4].unpack1("N")]
|
|
370
|
+
end
|
|
371
|
+
if header.start_with?("GIF87a".b, "GIF89a".b)
|
|
372
|
+
return header[6, 4].unpack("v2")
|
|
373
|
+
end
|
|
374
|
+
if header.start_with?("RIFF".b) && header[8, 4] == "WEBP".b
|
|
375
|
+
return read_webp_size(file, header)
|
|
376
|
+
end
|
|
377
|
+
return read_jpeg_size(file) if header.start_with?("\xFF\xD8".b)
|
|
378
|
+
end
|
|
379
|
+
nil
|
|
380
|
+
rescue StandardError => e
|
|
381
|
+
log_error("#{path}: could not read image dimensions: #{e.class}: #{e.message}")
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def read_webp_size(file, header)
|
|
386
|
+
format = header[12, 4]
|
|
387
|
+
if format == "VP8X".b
|
|
388
|
+
file.seek(24)
|
|
389
|
+
data = file.read(6)
|
|
390
|
+
return nil unless data&.bytesize == 6
|
|
391
|
+
|
|
392
|
+
bytes = data.bytes
|
|
393
|
+
width = 1 + (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16))
|
|
394
|
+
height = 1 + (bytes[3] | (bytes[4] << 8) | (bytes[5] << 16))
|
|
395
|
+
[width, height]
|
|
396
|
+
elsif format == "VP8 ".b
|
|
397
|
+
file.seek(26)
|
|
398
|
+
data = file.read(4)
|
|
399
|
+
return nil unless data&.bytesize == 4
|
|
400
|
+
|
|
401
|
+
width = data[0, 2].unpack1("v") & 0x3fff
|
|
402
|
+
height = data[2, 2].unpack1("v") & 0x3fff
|
|
403
|
+
[width, height]
|
|
404
|
+
elsif format == "VP8L".b
|
|
405
|
+
file.seek(21)
|
|
406
|
+
data = file.read(4)
|
|
407
|
+
return nil unless data&.bytesize == 4
|
|
408
|
+
|
|
409
|
+
b0, b1, b2, b3 = data.bytes
|
|
410
|
+
width = 1 + (((b1 & 0x3F) << 8) | b0)
|
|
411
|
+
height = 1 + (((b3 & 0x0F) << 10) | (b2 << 2) | ((b1 & 0xC0) >> 6))
|
|
412
|
+
[width, height]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def read_jpeg_size(file)
|
|
417
|
+
file.seek(2)
|
|
418
|
+
loop do
|
|
419
|
+
byte = file.read(1)
|
|
420
|
+
return nil unless byte == "\xFF".b
|
|
421
|
+
|
|
422
|
+
marker = nil
|
|
423
|
+
loop do
|
|
424
|
+
byte = file.read(1)
|
|
425
|
+
return nil unless byte
|
|
426
|
+
next if byte == "\xFF".b
|
|
427
|
+
|
|
428
|
+
marker = byte.ord
|
|
429
|
+
break
|
|
430
|
+
end
|
|
431
|
+
next if marker == 0xD8 || marker == 0xD9
|
|
432
|
+
return nil if marker == 0xDA
|
|
433
|
+
|
|
434
|
+
length_bytes = file.read(2)
|
|
435
|
+
return nil unless length_bytes&.bytesize == 2
|
|
436
|
+
|
|
437
|
+
length = length_bytes.unpack1("n")
|
|
438
|
+
return nil if length < 2
|
|
439
|
+
|
|
440
|
+
if [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].include?(marker)
|
|
441
|
+
data = file.read(5)
|
|
442
|
+
return nil unless data&.bytesize == 5
|
|
443
|
+
|
|
444
|
+
_precision, height, width = data.unpack("Cnn")
|
|
445
|
+
return [width, height]
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
file.seek(length - 2, IO::SEEK_CUR)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def page_label(page)
|
|
453
|
+
return page.relative_path if page.respond_to?(:relative_path) && page.relative_path
|
|
454
|
+
return page.path if page.respond_to?(:path) && page.path
|
|
455
|
+
|
|
456
|
+
"page"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def markup_context(markup)
|
|
460
|
+
src = markup[/src="([^"]*)"/, 1]
|
|
461
|
+
regions = markup[/data-jil-regions="([^"]*)"/, 1]
|
|
462
|
+
[regions, src].compact.reject(&:empty?).join(" ")
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def log_error(message)
|
|
466
|
+
Jekyll.logger.error("jekyll-image-links:", message)
|
|
467
|
+
end
|
|
468
|
+
|
|
303
469
|
def validate_dimensions!(width, height, src)
|
|
304
470
|
unless width.positive? && height.positive?
|
|
305
471
|
raise ArgumentError, "image_map requires native width and height in the YAML file or numeric width/height attributes"
|
|
@@ -15,14 +15,22 @@ module Jekyll
|
|
|
15
15
|
cfg = (site.config["image_links"] || {})
|
|
16
16
|
regions_body = @attrs["file"] ? nil : super
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
begin
|
|
19
|
+
figure = MapRenderer.render_portable_figure(
|
|
20
|
+
@attrs,
|
|
21
|
+
site: site,
|
|
22
|
+
page: context.registers[:page],
|
|
23
|
+
liquid_context: context,
|
|
24
|
+
cfg: cfg,
|
|
25
|
+
regions_body: regions_body
|
|
26
|
+
)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
page = context.registers[:page]
|
|
29
|
+
page_label = page.respond_to?(:relative_path) ? page.relative_path : "page"
|
|
30
|
+
src = @attrs["src"] || @attrs["file"]
|
|
31
|
+
Jekyll.logger.error("jekyll-image-links:", "#{page_label} #{src}: #{e.class}: #{e.message}")
|
|
32
|
+
raise
|
|
33
|
+
end
|
|
26
34
|
|
|
27
35
|
<<~HTML
|
|
28
36
|
{::nomarkdown}
|