草庐IT

javascript - 如何使用 HTML Canvas 执行洪水填充?

coder 2024-07-15 原文

有没有人在 javascript 中实现了洪水填充算法以与 HTML Canvas 一起使用?

我的要求很简单:从一个点开始用单一颜色填充,其中边界颜色是大于指定点颜色的某个增量的任何颜色。

var r1, r2; // red values
var g1, g2; // green values
var b1, b2; // blue values
var actualColorDelta = Math.sqrt((r1 - r2)*(r1 - r2) + (g1 - g2)*(g1 - g2) + (b1 - b2)*(b1 - b2))

function floodFill(canvas, x, y, fillColor, borderColorDelta) {
  ...
}

更新:

我自己编写了洪水填充的实现,如下所示。它很慢,但准确。大约 37% 的时间用于作为原型(prototype)框架一部分的两个低级数组函数。我想它们是由 push 和 pop 调用的。其余大部分时间都花在主循环中。
var ImageProcessing;

ImageProcessing = {

  /* Convert HTML color (e.g. "#rrggbb" or "#rrggbbaa") to object with properties r, g, b, a. 
   * If no alpha value is given, 255 (0xff) will be assumed.
   */
  toRGB: function (color) {
    var r, g, b, a, html;
    html = color;

    // Parse out the RGBA values from the HTML Code
    if (html.substring(0, 1) === "#")
    {
      html = html.substring(1);
    }

    if (html.length === 3 || html.length === 4)
    {
      r = html.substring(0, 1);
      r = r + r;

      g = html.substring(1, 2);
      g = g + g;

      b = html.substring(2, 3);
      b = b + b;

      if (html.length === 4) {
        a = html.substring(3, 4);
        a = a + a;
      }
      else {
        a = "ff";
      }
    }
    else if (html.length === 6 || html.length === 8)
    {
      r = html.substring(0, 2);
      g = html.substring(2, 4);
      b = html.substring(4, 6);
      a = html.length === 6 ? "ff" : html.substring(6, 8);
    }

    // Convert from Hex (Hexidecimal) to Decimal
    r = parseInt(r, 16);
    g = parseInt(g, 16);
    b = parseInt(b, 16);
    a = parseInt(a, 16);
    return {r: r, g: g, b: b, a: a};
  },

  /* Get the color at the given x,y location from the pixels array, assuming the array has a width and height as given.
   * This interprets the 1-D array as a 2-D array.
   *
   * If useColor is defined, its values will be set. This saves on object creation.
   */
  getColor: function (pixels, x, y, width, height, useColor) {
    var redIndex = y * width * 4 + x * 4;
    if (useColor === undefined) {
      useColor = { r: pixels[redIndex], g: pixels[redIndex + 1], b: pixels[redIndex + 2], a: pixels[redIndex + 3] };
    }
    else {
      useColor.r = pixels[redIndex];
      useColor.g = pixels[redIndex + 1]
      useColor.b = pixels[redIndex + 2];
      useColor.a = pixels[redIndex + 3];
    }
    return useColor;
  },

  setColor: function (pixels, x, y, width, height, color) {
    var redIndex = y * width * 4 + x * 4;
    pixels[redIndex] = color.r; 
    pixels[redIndex + 1] = color.g, 
    pixels[redIndex + 2] = color.b;
    pixels[redIndex + 3] = color.a;
  },

/*
 * fill: Flood a canvas with the given fill color.
 *
 * Returns a rectangle { x, y, width, height } that defines the maximum extent of the pixels that were changed.
 *
 *    canvas .................... Canvas to modify.
 *    fillColor ................. RGBA Color to fill with.
 *                                This may be a string ("#rrggbbaa") or an object of the form { r: red, g: green, b: blue, a: alpha }.
 *    x, y ...................... Coordinates of seed point to start flooding.
 *    bounds .................... Restrict flooding to this rectangular region of canvas. 
 *                                This object has these attributes: { x, y, width, height }.
 *                                If undefined or null, use the whole of the canvas.
 *    stopFunction .............. Function that decides if a pixel is a boundary that should cause
 *                                flooding to stop. If omitted, any pixel that differs from seedColor
 *                                will cause flooding to stop. seedColor is the color under the seed point (x,y).
 *                                Parameters: stopFunction(fillColor, seedColor, pixelColor).
 *                                Returns true if flooding shoud stop.
 *                                The colors are objects of the form { r: red, g: green, b: blue, a: alpha }
 */
 fill: function (canvas, fillColor, x, y, bounds, stopFunction) {
    // Supply default values if necessary.
    var ctx, minChangedX, minChangedY, maxChangedX, maxChangedY, wasTested, shouldTest, imageData, pixels, currentX, currentY, currentColor, currentIndex, seedColor, tryX, tryY, tryIndex, boundsWidth, boundsHeight, pixelStart, fillRed, fillGreen, fillBlue, fillAlpha;
    if (Object.isString(fillColor)) {
      fillColor = ImageProcessing.toRGB(fillColor);
    }
    x = Math.round(x);
    y = Math.round(y);
    if (bounds === null || bounds === undefined) {
      bounds = { x: 0, y: 0, width: canvas.width, height: canvas.height };
    }
    else {
      bounds = { x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.y), height: Math.round(bounds.height) };
    }
    if (stopFunction === null || stopFunction === undefined) {
      stopFunction = new function (fillColor, seedColor, pixelColor) {
        return pixelColor.r != seedColor.r || pixelColor.g != seedColor.g || pixelColor.b != seedColor.b || pixelColor.a != seedColor.a;
      }
    }
    minChangedX = maxChangedX = x - bounds.x;
    minChangedY = maxChangedY = y - bounds.y;
    boundsWidth = bounds.width;
    boundsHeight = bounds.height;

    // Initialize wasTested to false. As we check each pixel to decide if it should be painted with the new color,
    // we will mark it with a true value at wasTested[row = y][column = x];
    wasTested = new Array(boundsHeight * boundsWidth);
    /*
    $R(0, bounds.height - 1).each(function (row) { 
      var subArray = new Array(bounds.width);
      wasTested[row] = subArray;
    });
    */

    // Start with a single point that we know we should test: (x, y). 
    // Convert (x,y) to image data coordinates by subtracting the bounds' origin.
    currentX = x - bounds.x;
    currentY = y - bounds.y;
    currentIndex = currentY * boundsWidth + currentX;
    shouldTest = [ currentIndex ];

    ctx = canvas.getContext("2d");
    //imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
    imageData = ImageProcessing.getImageData(ctx, bounds.x, bounds.y, bounds.width, bounds.height);
    pixels = imageData.data;
    seedColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight);
    currentColor = { r: 0, g: 0, b: 0, a: 1 };
    fillRed = fillColor.r;
    fillGreen = fillColor.g;
    fillBlue = fillColor.b;
    fillAlpha = fillColor.a;
    while (shouldTest.length > 0) {
      currentIndex = shouldTest.pop();
      currentX = currentIndex % boundsWidth;
      currentY = (currentIndex - currentX) / boundsWidth;
      if (! wasTested[currentIndex]) {
        wasTested[currentIndex] = true;
        //currentColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight, currentColor);
        // Inline getColor for performance.
        pixelStart = currentIndex * 4;
        currentColor.r = pixels[pixelStart];
        currentColor.g = pixels[pixelStart + 1]
        currentColor.b = pixels[pixelStart + 2];
        currentColor.a = pixels[pixelStart + 3];

        if (! stopFunction(fillColor, seedColor, currentColor)) {
          // Color the pixel with the fill color. 
          //ImageProcessing.setColor(pixels, currentX, currentY, boundsWidth, boundsHeight, fillColor);
          // Inline setColor for performance
          pixels[pixelStart] = fillRed;
          pixels[pixelStart + 1] = fillGreen;
          pixels[pixelStart + 2] = fillBlue;
          pixels[pixelStart + 3] = fillAlpha;

          if (minChangedX < currentX) { minChangedX = currentX; }
          else if (maxChangedX > currentX) { maxChangedX = currentX; }
          if (minChangedY < currentY) { minChangedY = currentY; }
          else if (maxChangedY > currentY) { maxChangedY = currentY; }

          // Add the adjacent four pixels to the list to be tested, unless they have already been tested.
          tryX = currentX - 1;
          tryY = currentY;
          tryIndex = tryY * boundsWidth + tryX;
          if (tryX >= 0 && ! wasTested[tryIndex]) {
            shouldTest.push(tryIndex); 
          }
          tryX = currentX;
          tryY = currentY + 1;
          tryIndex = tryY * boundsWidth + tryX;
          if (tryY < boundsHeight && ! wasTested[tryIndex]) {
            shouldTest.push(tryIndex); 
          }
          tryX = currentX + 1;
          tryY = currentY;
          tryIndex = tryY * boundsWidth + tryX;
          if (tryX < boundsWidth && ! wasTested[tryIndex]) {
            shouldTest.push(tryIndex); 
          }
          tryX = currentX;
          tryY = currentY - 1;
          tryIndex = tryY * boundsWidth + tryX;
          if (tryY >= 0 && ! wasTested[tryIndex]) {
            shouldTest.push(tryIndex); 
          }
        }
      }
    }
    //ctx.putImageData(imageData, bounds.x, bounds.y);
    ImageProcessing.putImageData(ctx, imageData, bounds.x, bounds.y);

    return { x: minChangedX + bounds.x, y: minChangedY + bounds.y, width: maxChangedX - minChangedX + 1, height: maxChangedY - minChangedY + 1 };
  },

  getImageData: function (ctx, x, y, w, h) { 
    return ctx.getImageData(x, y, w, h); 
  },

  putImageData: function (ctx, data, x, y) { 
    ctx.putImageData(data, x, y); 
  }

};

顺便说一句,当我调用它时,我使用自定义 stopFunction:
  stopFill : function (fillColor, seedColor, pixelColor) {
    // Ignore alpha difference for now.
    return Math.abs(pixelColor.r - seedColor.r) > this.colorTolerance || Math.abs(pixelColor.g - seedColor.g) > this.colorTolerance || Math.abs(pixelColor.b - seedColor.b) > this.colorTolerance;
  },

如果有人能看到提高此代码性能的方法,我将不胜感激。基本思想是:
1) 种子颜色是开始浸水时的初始颜色。
2) 尝试四个相邻点:上、右、下、左一个像素。
3) 如果点超出范围或已经被访问过,则跳过它。
4) 否则将点推到有趣点的堆栈上。
5) 从堆栈中弹出下一个有趣的点。
6) 如果该点的颜色是停止颜色(在 stopFunction 中定义),则停止处理该点并跳到步骤 5。
7) 否则,跳到第 2 步。
8)当没有更多有趣的点可以访问时,停止循环。

记住一个点已被访问需要一个元素数与像素数相同的数组。

最佳答案

要创建洪水填充,您需要能够查看已经存在的像素并检查它们是否不是您开始时使用的颜色,例如这样。

const ctx = document.querySelector("canvas").getContext("2d");

ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();

floodFill(ctx, 40, 50, [255, 0, 0, 255]);

function getPixel(imageData, x, y) {
  if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
    return [-1, -1, -1, -1];  // impossible color
  } else {
    const offset = (y * imageData.width + x) * 4;
    return imageData.data.slice(offset, offset + 4);
  }
}

function setPixel(imageData, x, y, color) {
  const offset = (y * imageData.width + x) * 4;
  imageData.data[offset + 0] = color[0];
  imageData.data[offset + 1] = color[1];
  imageData.data[offset + 2] = color[2];
  imageData.data[offset + 3] = color[0];
}

function colorsMatch(a, b) {
  return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}

function floodFill(ctx, x, y, fillColor) {
  // read the pixels in the canvas
  const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  // get the color we're filling
  const targetColor = getPixel(imageData, x, y);
  
  // check we are actually filling a different color
  if (!colorsMatch(targetColor, fillColor)) {
  
    fillPixel(imageData, x, y, targetColor, fillColor);
    
    // put the data back
    ctx.putImageData(imageData, 0, 0);
  }
}

function fillPixel(imageData, x, y, targetColor, fillColor) {
  const currentColor = getPixel(imageData, x, y);
  if (colorsMatch(currentColor, targetColor)) {
    setPixel(imageData, x, y, fillColor);
    fillPixel(imageData, x + 1, y, targetColor, fillColor);
    fillPixel(imageData, x - 1, y, targetColor, fillColor);
    fillPixel(imageData, x, y + 1, targetColor, fillColor);
    fillPixel(imageData, x, y - 1, targetColor, fillColor);
  }
}
<canvas></canvas>

不过,这段代码至少有 2 个问题。
  • 它是深度递归的。
    所以你可能会用完堆栈空间
  • 它很慢。
    不知道它是否太慢,但浏览器中的 JavaScript 主要是单线程的,所以当这段代码运行时,浏览器会被卡住。对于大 Canvas ,卡住时间可能会使页面变得非常慢,如果卡住时间过长,浏览器会询问用户是否要终止页面。

  • 堆栈空间不足的解决方案是实现我们自己的堆栈。例如,而不是递归调用 fillPixel我们可以保留一系列我们想要查看的位置。我们将 4 个位置添加到该数组中,然后从数组中弹出内容直到它为空

    const ctx = document.querySelector("canvas").getContext("2d");
    
    ctx.beginPath();
    ctx.moveTo(20, 20);
    ctx.lineTo(250, 70);
    ctx.lineTo(270, 120);
    ctx.lineTo(170, 140);
    ctx.lineTo(190, 80);
    ctx.lineTo(100, 60);
    ctx.lineTo(50, 130);
    ctx.lineTo(20, 20);
    ctx.stroke();
    
    floodFill(ctx, 40, 50, [255, 0, 0, 255]);
    
    function getPixel(imageData, x, y) {
      if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
        return [-1, -1, -1, -1];  // impossible color
      } else {
        const offset = (y * imageData.width + x) * 4;
        return imageData.data.slice(offset, offset + 4);
      }
    }
    
    function setPixel(imageData, x, y, color) {
      const offset = (y * imageData.width + x) * 4;
      imageData.data[offset + 0] = color[0];
      imageData.data[offset + 1] = color[1];
      imageData.data[offset + 2] = color[2];
      imageData.data[offset + 3] = color[0];
    }
    
    function colorsMatch(a, b) {
      return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
    }
    
    function floodFill(ctx, x, y, fillColor) {
      // read the pixels in the canvas
      const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      
      // get the color we're filling
      const targetColor = getPixel(imageData, x, y);
      
      // check we are actually filling a different color
      if (!colorsMatch(targetColor, fillColor)) {
      
        const pixelsToCheck = [x, y];
        while (pixelsToCheck.length > 0) {
          const y = pixelsToCheck.pop();
          const x = pixelsToCheck.pop();
          
          const currentColor = getPixel(imageData, x, y);
          if (colorsMatch(currentColor, targetColor)) {
            setPixel(imageData, x, y, fillColor);
            pixelsToCheck.push(x + 1, y);
            pixelsToCheck.push(x - 1, y);
            pixelsToCheck.push(x, y + 1);
            pixelsToCheck.push(x, y - 1);
          }
        }
        
        // put the data back
        ctx.putImageData(imageData, 0, 0);
      }
    }
    <canvas></canvas>

    解决它太慢的方法是 make it run a little at a time或将其移至 worker 处。我认为这有点太多了,无法在相同的答案中显示 here's an example .
    我在 4096x4096 Canvas 上测试了上面的代码,在我的机器上填充空白 Canvas 需要 16 秒,所以是的,它可以说太慢了,但是把它放在一个 worker 中会带来新的问题,即结果将是异步的,所以即使浏览器不会卡住您可能希望阻止用户在完成之前做某事。
    另一个问题是您会看到线条是抗锯齿的,因此用纯色填充会关闭线条,但不会一直到它。要解决此问题,您可以更改 colorsMatch检查是否足够接近,但是如果targetColor,您会遇到一个新问题和 fillColor也足够接近它会继续尝试填满自己。您可以通过创建另一个数组来解决这个问题,每个像素一个字节或一个位来跟踪您准备检查的位置。

    const ctx = document.querySelector("canvas").getContext("2d");
    
    ctx.beginPath();
    ctx.moveTo(20, 20);
    ctx.lineTo(250, 70);
    ctx.lineTo(270, 120);
    ctx.lineTo(170, 140);
    ctx.lineTo(190, 80);
    ctx.lineTo(100, 60);
    ctx.lineTo(50, 130);
    ctx.lineTo(20, 20);
    ctx.stroke();
    
    floodFill(ctx, 40, 50, [255, 0, 0, 255], 128);
    
    function getPixel(imageData, x, y) {
      if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
        return [-1, -1, -1, -1];  // impossible color
      } else {
        const offset = (y * imageData.width + x) * 4;
        return imageData.data.slice(offset, offset + 4);
      }
    }
    
    function setPixel(imageData, x, y, color) {
      const offset = (y * imageData.width + x) * 4;
      imageData.data[offset + 0] = color[0];
      imageData.data[offset + 1] = color[1];
      imageData.data[offset + 2] = color[2];
      imageData.data[offset + 3] = color[0];
    }
    
    function colorsMatch(a, b, rangeSq) {
      const dr = a[0] - b[0];
      const dg = a[1] - b[1];
      const db = a[2] - b[2];
      const da = a[3] - b[3];
      return dr * dr + dg * dg + db * db + da * da < rangeSq;
    }
    
    function floodFill(ctx, x, y, fillColor, range = 1) {
      // read the pixels in the canvas
      const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      
      // flags for if we visited a pixel already
      const visited = new Uint8Array(imageData.width, imageData.height);
      
      // get the color we're filling
      const targetColor = getPixel(imageData, x, y);
      
      // check we are actually filling a different color
      if (!colorsMatch(targetColor, fillColor)) {
    
        const rangeSq = range * range;
        const pixelsToCheck = [x, y];
        while (pixelsToCheck.length > 0) {
          const y = pixelsToCheck.pop();
          const x = pixelsToCheck.pop();
          
          const currentColor = getPixel(imageData, x, y);
          if (!visited[y * imageData.width + x] &&
               colorsMatch(currentColor, targetColor, rangeSq)) {
            setPixel(imageData, x, y, fillColor);
            visited[y * imageData.width + x] = 1;  // mark we were here already
            pixelsToCheck.push(x + 1, y);
            pixelsToCheck.push(x - 1, y);
            pixelsToCheck.push(x, y + 1);
            pixelsToCheck.push(x, y - 1);
          }
        }
        
        // put the data back
        ctx.putImageData(imageData, 0, 0);
      }
    }
    <canvas></canvas>

    注意这个版本的colorsMatch有点天真。转换为 HSV 或其他东西可能会更好,或者您可能想按 alpha 加权。我不知道什么是匹配颜色的好指标。
    更新
    另一种加快速度的方法当然是优化代码。 Kaiido 指出一个明显的加速是使用 Uint32Array查看像素。这样查找一个像素并设置一个像素,只有一个 32 位值可供读取或写入。 Just that change makes it about 4x faster .尽管如此,填充 4096x4096 Canvas 仍然需要 4 秒。可能还有其他优化,比如不是调用 getPixels使其内联,但不要在我们的像素列表中推送新像素以检查它们是否超出范围。它可能会提高 10% 的速度(不知道),但不会使其足够快以达到交互速度。
    还有其他加速,比如一次检查一行,因为行是缓存友好的,你可以计算一次行的偏移量,并在检查整行时使用它,而现在对于每个像素,我们必须多次计算偏移量。
    这些会使算法复杂化,因此最好让您自己弄清楚。
    我还要补充一点,鉴于上面的答案在填充发生时会卡住浏览器,并且在卡住时间可能过长的较大 Canvas 上,您可以使用 ES6 异步/等待轻松地使算法跨越时间。您需要选择为每个时间段分配多少工作。选择太小,填满需要很长时间。选择太大,浏览器卡住时会卡顿。
    这是一个例子。套装ticksPerUpdate加快或减慢填充率

    const ctx = document.querySelector("canvas").getContext("2d");
    
    ctx.beginPath();
    ctx.moveTo(20, 20);
    ctx.lineTo(250, 70);
    ctx.lineTo(270, 120);
    ctx.lineTo(170, 140);
    ctx.lineTo(100, 145);
    ctx.lineTo(110, 105);
    ctx.lineTo(130, 125);
    ctx.lineTo(190, 80);
    ctx.lineTo(100, 60);
    ctx.lineTo(50, 130);
    ctx.lineTo(20, 20);
    ctx.stroke();
    
    floodFill(ctx, 40, 50, 0xFF0000FF);
    
    function getPixel(pixelData, x, y) {
      if (x < 0 || y < 0 || x >= pixelData.width || y >= pixelData.height) {
        return -1;  // impossible color
      } else {
        return pixelData.data[y * pixelData.width + x];
      }
    }
    
    async function floodFill(ctx, x, y, fillColor) {
      // read the pixels in the canvas
      const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      
      // make a Uint32Array view on the pixels so we can manipulate pixels
      // one 32bit value at a time instead of as 4 bytes per pixel
      const pixelData = {
        width: imageData.width,
        height: imageData.height,
        data: new Uint32Array(imageData.data.buffer),
      };
      
      // get the color we're filling
      const targetColor = getPixel(pixelData, x, y);
      
      // check we are actually filling a different color
      if (targetColor !== fillColor) {
      
        const ticksPerUpdate = 50;
        let tickCount = 0;
        const pixelsToCheck = [x, y];
        while (pixelsToCheck.length > 0) {
          const y = pixelsToCheck.pop();
          const x = pixelsToCheck.pop();
          
          const currentColor = getPixel(pixelData, x, y);
          if (currentColor === targetColor) {
            pixelData.data[y * pixelData.width + x] = fillColor;
            
            // put the data back
            ctx.putImageData(imageData, 0, 0);
            ++tickCount;
            if (tickCount % ticksPerUpdate === 0) {
              await wait();
            }
            
            pixelsToCheck.push(x + 1, y);
            pixelsToCheck.push(x - 1, y);
            pixelsToCheck.push(x, y + 1);
            pixelsToCheck.push(x, y - 1);
          }
        }    
      }
    }
    
    function wait(delay = 0) {
      return new Promise((resolve) => {
        setTimeout(resolve, delay);
      });
    }
    <canvas></canvas>

    关于javascript - 如何使用 HTML Canvas 执行洪水填充?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2106995/

    有关javascript - 如何使用 HTML Canvas 执行洪水填充?的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    4. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    5. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    6. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    9. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    10. ruby-openid:执行发现时未设置@socket - 2

      我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

    随机推荐