support embedding blocks into PNG metadata

snap8
Jens Mönig 2022-04-22 16:11:15 +02:00
rodzic 789e257c0b
commit 1e18b5b1ad
5 zmienionych plików z 133 dodań i 16 usunięć

Wyświetl plik

@ -41,6 +41,9 @@
* **Translation Updates:** * **Translation Updates:**
* German * German
### 2022-04-22
* morphic, objects, gui: support embedding blocks into PNG metadata
### 2022-04-20 ### 2022-04-20
* threads: terminate all threads waiting to display a question on ASKing a falsy value * threads: terminate all threads waiting to display a question on ASKing a falsy value
* threads: clear "answer" on ASK nothing/falsy * threads: clear "answer" on ASK nothing/falsy

Wyświetl plik

@ -13,14 +13,14 @@
<meta name="apple-mobile-web-app-title" content="Snap!"> <meta name="apple-mobile-web-app-title" content="Snap!">
<meta name="msapplication-TileImage" content="img/snap-icon-144.png"> <meta name="msapplication-TileImage" content="img/snap-icon-144.png">
<meta name="msapplication-TileColor" content="#FFFFFF"> <meta name="msapplication-TileColor" content="#FFFFFF">
<script src="src/morphic.js?version=2022-01-28"></script> <script src="src/morphic.js?version=2022-04-22"></script>
<script src="src/symbols.js?version=2021-03-03"></script> <script src="src/symbols.js?version=2021-03-03"></script>
<script src="src/widgets.js?version=2021-17-09"></script> <script src="src/widgets.js?version=2021-17-09"></script>
<script src="src/blocks.js?version=2022-04-20"></script> <script src="src/blocks.js?version=2022-04-20"></script>
<script src="src/threads.js?version=2022-04-20"></script> <script src="src/threads.js?version=2022-04-20"></script>
<script src="src/objects.js?version=2022-04-20"></script> <script src="src/objects.js?version=2022-04-22"></script>
<script src="src/scenes.js?version=2022-03-03"></script> <script src="src/scenes.js?version=2022-03-03"></script>
<script src="src/gui.js?version=2022-04-06"></script> <script src="src/gui.js?version=2022-04-22"></script>
<script src="src/paint.js?version=2021-07-05"></script> <script src="src/paint.js?version=2021-07-05"></script>
<script src="src/lists.js?version=2022-02-07"></script> <script src="src/lists.js?version=2022-02-07"></script>
<script src="src/byob.js?version=2022-04-20"></script> <script src="src/byob.js?version=2022-04-20"></script>

Wyświetl plik

@ -86,7 +86,7 @@ BlockVisibilityDialogMorph, ThreadManager*/
// Global stuff //////////////////////////////////////////////////////// // Global stuff ////////////////////////////////////////////////////////
modules.gui = '2022-April-20'; modules.gui = '2022-April-22';
// Declarations // Declarations
@ -2471,7 +2471,7 @@ IDE_Morph.prototype.endBulkDrop = function () {
this.bulkDropInProgress = false; this.bulkDropInProgress = false;
}; };
IDE_Morph.prototype.droppedImage = function (aCanvas, name) { IDE_Morph.prototype.droppedImage = function (aCanvas, name, embeddedCode) {
var costume = new Costume( var costume = new Costume(
aCanvas, aCanvas,
this.currentSprite.newCostumeName( this.currentSprite.newCostumeName(
@ -2491,6 +2491,7 @@ IDE_Morph.prototype.droppedImage = function (aCanvas, name) {
return; return;
} }
costume.code = embeddedCode || null;
this.currentSprite.addCostume(costume); this.currentSprite.addCostume(costume);
this.currentSprite.wearCostume(costume); this.currentSprite.wearCostume(costume);
this.spriteBar.tabBar.tabTo('costumes'); this.spriteBar.tabBar.tabTo('costumes');
@ -10063,6 +10064,9 @@ CostumeIconMorph.prototype.exportCostume = function () {
if (this.object instanceof SVG_Costume) { if (this.object instanceof SVG_Costume) {
// don't show SVG costumes in a new tab (shows text) // don't show SVG costumes in a new tab (shows text)
ide.saveFileAs(this.object.contents.src, 'text/svg', this.object.name); ide.saveFileAs(this.object.contents.src, 'text/svg', this.object.name);
} else if (this.object.code) {
// embed blocks code inside the PNG image data
ide.saveFileAs(this.object.pngData(), 'image/png', this.object.name);
} else { // rasterized Costume } else { // rasterized Costume
ide.saveCanvasAs(this.object.contents, this.object.name); ide.saveCanvasAs(this.object.contents, this.object.name);
} }

Wyświetl plik

@ -642,7 +642,7 @@
Drops of image elements from outside the world canvas are dispatched as Drops of image elements from outside the world canvas are dispatched as
droppedImage(aCanvas, name) droppedImage(aCanvas, name, embeddedCode)
droppedSVG(anImage, name) droppedSVG(anImage, name)
events to interested Morphs at the mouse pointer. If you want your Morph events to interested Morphs at the mouse pointer. If you want your Morph
@ -663,8 +663,22 @@
droppedImage() event with a canvas containing a rasterized version of the droppedImage() event with a canvas containing a rasterized version of the
SVG. SVG.
The same applies to drops of audio or text files from outside the world Note that PNG images provide for embedded text comments, which can be used
canvas. to include code inside the image. Such a payload has to be identified by
an agreed-upon marker. The default tag is stored in MorphicPreferences and
can be overriden by apps wishing to make use of this feature. If such an
embedded text-payload is found inside a PNG it is passed as the optional
third "embeddedCode" parameter to the "droppedImage()" event. embedded text
only applies to PNGs. You can embed a string into the PNG metadata of a PNG
by calling
embedMetadataPNG(aCanvas, aString)
with a raster image represented by a canvas and a string that is to be
embedded into the PNG's metadata.
The same event mechanism applies to drops of audio or text files from
outside the world canvas.
Those are dispatched as Those are dispatched as
@ -1276,6 +1290,8 @@
Jason N (@cyderize) contributed native copy & paste for text editing. Jason N (@cyderize) contributed native copy & paste for text editing.
Bartosz Leper contributed retina display support. Bartosz Leper contributed retina display support.
Zhenlei Jia and Dariusz Dorożalski pioneered IME text editing. Zhenlei Jia and Dariusz Dorożalski pioneered IME text editing.
Dariusz Dorożalski and Jesus Villalobos contributed embedding blocks
into image metadata.
Bernat Romagosa contributed to text editing and to the core design. Bernat Romagosa contributed to text editing and to the core design.
Michael Ball found and fixed a longstanding scrolling bug. Michael Ball found and fixed a longstanding scrolling bug.
Brian Harvey contributed to the design and implementation of submenus. Brian Harvey contributed to the design and implementation of submenus.
@ -1289,9 +1305,9 @@
/*global window, HTMLCanvasElement, FileReader, Audio, FileList, Map*/ /*global window, HTMLCanvasElement, FileReader, Audio, FileList, Map*/
/*jshint esversion: 6*/ /*jshint esversion: 11, bitwise: false*/
var morphicVersion = '2022-January-28'; var morphicVersion = '2022-April-22';
var modules = {}; // keep track of additional loaded modules var modules = {}; // keep track of additional loaded modules
var useBlurredShadows = true; var useBlurredShadows = true;
@ -1303,6 +1319,7 @@ const CLEAR = new Color(0, 0, 0, 0);
Object.freeze(ZERO); Object.freeze(ZERO);
Object.freeze(BLACK); Object.freeze(BLACK);
Object.freeze(WHITE); Object.freeze(WHITE);
Object.freeze(CLEAR);
var standardSettings = { var standardSettings = {
minimumFontHeight: getMinimumFontHeight(), // browser settings minimumFontHeight: getMinimumFontHeight(), // browser settings
@ -1318,6 +1335,7 @@ var standardSettings = {
mouseScrollAmount: 40, mouseScrollAmount: 40,
useSliderForInput: false, useSliderForInput: false,
isTouchDevice: false, // turned on by touch events, don't set isTouchDevice: false, // turned on by touch events, don't set
pngPayloadMarker: 'Data\tPayload\tEmbedded',
rasterizeSVGs: false, rasterizeSVGs: false,
isFlat: false, isFlat: false,
grabThreshold: 5, grabThreshold: 5,
@ -1338,6 +1356,7 @@ var touchScreenSettings = {
mouseScrollAmount: 40, mouseScrollAmount: 40,
useSliderForInput: false, useSliderForInput: false,
isTouchDevice: true, isTouchDevice: true,
pngPayloadMarker: 'Data\tPayload\tEmbedded',
rasterizeSVGs: false, rasterizeSVGs: false,
isFlat: false, isFlat: false,
grabThreshold: 5, grabThreshold: 5,
@ -1569,6 +1588,65 @@ function copy(target) {
return c; return c;
} }
function escapeString(str) {
var len, R = '', k = 0, S, chr, ord,
ascii = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +
'0123456789@*_+-./,';
str = str.toString();
len = str.length;
while(k < len) {
chr = str[k];
if (ascii.indexOf(chr) != -1) {
S = chr;
} else {
ord = str.charCodeAt(k);
if (ord < 256) {
S = '%' + ("00" + ord.toString(16)).toUpperCase().slice(-2);
} else {
S = '%u' + ("0000" + ord.toString(16)).toUpperCase().slice(-4);
}
}
R += S;
k++;
}
return R;
}
function embedMetadataPNG(aCanvas, aString) {
var embedTag = MorphicPreferences.pngPayloadMarker,
crc32 = (str, crc) => {
let table = [...Array(256).keys()].map(it =>
[...Array(8)].reduce((cc) =>
(cc & 1) ? (0xedb88320 ^ (cc >>> 1)) : (cc >>> 1), it)
);
crc = [...str].reduce(
(crc, ch) => (crc >>> 8) ^ table[(crc ^ ch.charCodeAt(0)) & 0xff],
(crc ? crc = 0 : crc) ^ (-1) // (crc ||= 0) ^ (-1)
);
return ( crc ^ (-1) ) >>> 0;
},
arr2Str = (arr) =>
arr.reduce((res, byte) => res + String.fromCharCode(byte), ''),
int2BStr = (val) =>
arr2Str(Array.from(new Uint8Array(new Uint32Array( [val] ).buffer)).reverse()),
buildChunk = (data) => {
let res = "iTXt" + data;
return int2BStr(data.length) + res + int2BStr(crc32(res));
},
parts = aCanvas.toDataURL("image/png").split(","),
bPart = atob(parts[1]).split(""),
newChunk = buildChunk(
"Snap!_SRC\0\0\0\0\0" +
embedTag +
aString +
embedTag
);
bPart.splice(-12, 0, ...newChunk);
parts[1] = btoa(bPart.join(""));
return parts.join(',');
}
// Retina Display Support ////////////////////////////////////////////// // Retina Display Support //////////////////////////////////////////////
/* /*
@ -11643,7 +11721,7 @@ HandMorph.prototype.processDrop = function (event) {
onto the world canvas, turn it into an offscreen canvas or audio onto the world canvas, turn it into an offscreen canvas or audio
element and dispatch the element and dispatch the
droppedImage(canvas, name) droppedImage(canvas, name, embeddedCode)
droppedSVG(image, name) droppedSVG(image, name)
droppedAudio(audio, name) droppedAudio(audio, name)
droppedText(text, name, type) droppedText(text, name, type)
@ -11692,16 +11770,41 @@ HandMorph.prototype.processDrop = function (event) {
function readImage(aFile) { function readImage(aFile) {
var pic = new Image(), var pic = new Image(),
frd = new FileReader(), frd = new FileReader(),
trg = target; url = event.dataTransfer?.getData( "text/uri-list"),
file = event.dataTransfer?.files?.[0],
trg = target,
embedTag = MorphicPreferences.pngPayloadMarker;
while (!trg.droppedImage) { while (!trg.droppedImage) {
trg = trg.parent; trg = trg.parent;
} }
pic.onload = () => { pic.onload = () => {
canvas = newCanvas(new Point(pic.width, pic.height), true); canvas = newCanvas(new Point(pic.width, pic.height), true);
canvas.getContext('2d').drawImage(pic, 0, 0); canvas.getContext('2d').drawImage(pic, 0, 0);
trg.droppedImage(canvas, aFile.name);
bulkDrop(); (async () => {
if (!file && url) {
// alternative: "https://api.allorigins.win/raw?url=" + url
file = await fetch(url);
}
// extract embedded data (e.g. blocks)
// from the image's meta data if present.
let buff = new Uint8Array(await file?.arrayBuffer()),
strBuff = buff.reduce((acc, b) =>
acc + String.fromCharCode(b), ""),
embedded = strBuff.includes(embedTag) ?
decodeURIComponent(
escapeString((strBuff)?.split(embedTag)[1])
)
: null;
trg.droppedImage(canvas, aFile.name, embedded);
bulkDrop();
})();
}; };
frd = new FileReader(); frd = new FileReader();
frd.onloadend = (e) => pic.src = e.target.result; frd.onloadend = (e) => pic.src = e.target.result;
frd.readAsDataURL(aFile); frd.readAsDataURL(aFile);

Wyświetl plik

@ -89,11 +89,12 @@ SpeechBubbleMorph, InputSlotMorph, isNil, FileReader, TableDialogMorph, String,
BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph, BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph,
localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph, localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph,
AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows, BLACK, AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows, BLACK,
BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph, MenuItemMorph*/ BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph, MenuItemMorph,
embedMetadataPNG*/
/*jshint esversion: 6*/ /*jshint esversion: 6*/
modules.objects = '2022-April-20'; modules.objects = '2022-April-22';
var SpriteMorph; var SpriteMorph;
var StageMorph; var StageMorph;
@ -10776,6 +10777,12 @@ Costume.prototype.isTainted = function () {
return false; return false;
}; };
// Costume storing blocks code in PNG exports
Costume.prototype.pngData = function () {
return embedMetadataPNG(this.contents, this.code);
};
// SVG_Costume ///////////////////////////////////////////////////////////// // SVG_Costume /////////////////////////////////////////////////////////////
/* /*