<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="https://www.w3.org/2005/Atom">
  <channel>
    <title>Carmalou.JS</title>
    <description>Dropping JavaScript knowledge, one post at a time.</description>
    <link>carmalou.com/</link>
    <atom:link href="carmalou.com/zfeed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Thu, 04 Sep 2025 01:03:42 +0000</pubDate>
    <lastBuildDate>Thu, 04 Sep 2025 01:03:42 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Parsing PNGs with Node, Part 4</title>
        <description>&lt;p&gt;We left off &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/09/03/parsing-pngs-pt-3.html&quot;&gt;the last post&lt;/a&gt; looping over each pixel, wahoo! So this final post will focus on ✨filter types✨.&lt;/p&gt;

&lt;p&gt;Here’s the image I’m working against:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/carmalou/image-scaling-exercise/refs/heads/main/sample_images/grayscale-parrot.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/blog_examples/filterTypes.js&quot;&gt;And here’s the final output of this post.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the last post, we were splitting the scanline into individual rows and looping over each byte in the row. Conveniently enough, the sample image has a 1:1 pixel to byte ratio, so we don’t need to do anything special to group bytes together to make a more complex pixel, or split them up. But keep in mind that won’t always be the case.&lt;/p&gt;

&lt;p&gt;You might’ve noticed in the last method we worked on, if a row was 150 pixels wide, we were always adding one additional byte to the row. That first byte of the row was added to a variable called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filterByte&lt;/code&gt;. (&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/blog_examples/loopingThePixelArray.js#L32&quot;&gt;You can refresh your memory here.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So what &lt;em&gt;is&lt;/em&gt; a filter byte? The concept is so clever! Let’s circle back to the baby blue rbg value I referenced in the last post: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;137, 207, 240&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/blue.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Converted to a byte array, that single pixel looks like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10001001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11001111&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11110000&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of course, this is just a single pixel, think about an entire image. We already talked about how unwieldy this could get, but consider how often a single color gets repeated in an image. Look at the sample grayscale image we’re using - there’s a lot of the same colors. Think about how much space a file could save if it could use one complete pixel value as a reference for other pixels - that is what Filter Types do. 🤯&lt;/p&gt;

&lt;p&gt;There are 5 different values for a filter type:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;0: No filter is applied. The values for this row are absolute values&lt;/li&gt;
  &lt;li&gt;1: Sub filtering - performs a look behind to the pixel directly to its left&lt;/li&gt;
  &lt;li&gt;2: Up filtering - performs a “look up” to the pixel directly above it&lt;/li&gt;
  &lt;li&gt;3: Average filtering - takes an average of the neighboring pixels&lt;/li&gt;
  &lt;li&gt;4: Paeth filtering - using neighboring pixels, Paeth is predictive in choosing the value closest to the computed value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s go through these one at a time. In the previous post, we left off looping over the row, but now let’s assume we’re passing the entire row to a function where the looping is performed.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;// note that this function will assume that currentRow DOES NOT include filterByte
+ function applyPixelFilter(filterType, bitDepth, currentRow, previousRow) {
+   switch(filterType) {
+     case 0: 
+     case 1:
+     case 2:
+     case 3:
+     case 4:
+   }
+ }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since a filter type of 0 means the pixels are already in their absolute form, we’ll start with filter type 1. As mentioned above, filter type 1 is performing a look-behind to it’s left-hand neighbor. The first row in a PNG might use this filter type, since it doesn’t have any neighbors above it for reference.&lt;/p&gt;

&lt;p&gt;When the look-behind is performed, the current pixel doesn’t just assume the value of the previous pixel - instead the value is the combined total of the left-hand pixel and the current pixel, up to 255. Note that this number wraps around, so any overflow beyond 255 is the actual value used.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;  /* ... */

  switch(filterType) {
    /* ... */

    case 1:
+   let unfilteredPixels = []

+   currentRow.forEach((value, i) =&amp;gt; {
+     if (i === 0) {
+       unfilteredPixels.push(value)
+       return
+     }

      // perform look-behind
+     unfilteredPixels.push(
+       (value + (unfilteredPixels[unfilteredPixels.length - 1] || 0)) % 256
+     )
+   })

+   return unfilteredPixels

    /* ... */
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In our first case (as we will in every case) we loop over the row. (Note that you need to check for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bitDepth&lt;/code&gt; to make sure that a pixel really is a single byte, as this assumes. I left that out to keep the code more approachable.)&lt;/p&gt;

&lt;p&gt;Within the loop, we are assuming that the first byte value is an absolute value, which is safe for a look-behind, because that pixel has no left-hand neighbor to reference. Moving on, we are using the last value from our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unfilteredPixels&lt;/code&gt; array as our reference. It’s important to note that as we are looping over the row, each previous pixel was also referencing &lt;em&gt;its&lt;/em&gt; left-hand neighbor. In order to calculate the correct value for the current pixel, we need to look at the absolute value of the previous pixel, which we receive after filtering, &lt;em&gt;not&lt;/em&gt; the provided value. &lt;strong&gt;This will be true for all filter types.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Filter type 2 is very similar, but instead of a look-behind, it performs a “look-up”. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;previousRow&lt;/code&gt; as our reference, we will combine the current pixel’s value and the absolute value from the same space in the previous row.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;  /* ... */

  switch(filterType) {
    /* ... */

    case 2:
+     let unfilteredPixels = []

+     currentRow.forEach((value, i) =&amp;gt; {
        const b = previousRow?.[i] || 0

+       unfilteredPixels.push((value + b) % 256)
+     })

+     return unfilteredPixels

    /* ... */
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Filter type 3 is a little bit more complicated, but it builds on types 1 and 2. Instead of adding the absolute value of a neighboring pixel, we take the average of two neighboring pixels - left and above - and add their average to the current pixel’s value.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;  /* ... */

  switch(filterType) {
    /* ... */

    case 3:
+   let unfilteredPixels = []

+   currentRow.forEach((value, i) =&amp;gt; {
+     if (i === 0) {
+       const b = previousRow[i] || 0
+       const avg = Math.floor(b / 2)

+       unfilteredPixels.push((value + avg) % 256)

+       return
+     }

+     const a = unfilteredPixels[unfilteredPixels.length - 1] || 0
+     const b = previousRow[i] || 0

+     const avg = Math.floor((a + b) / 2)

+     unfilteredPixels.push((value + avg) % 256)
+   })

+   return unfilteredPixels

    /* ... */
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Similar to case 1, we know that the first pixel in this row will not have a left-hand neighbor, so we take the average of the neighborhood above, and add that value to the current pixel. &lt;strong&gt;Remember that we are always taking absolute values &lt;em&gt;after&lt;/em&gt; the filtering has been applied.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Moving on in the loop, we take each reference value, and find their average. That average is then applied to the current pixel’s value.&lt;/p&gt;

&lt;p&gt;Lastly, we have Paeth filtering, which is the most complicated. It uses three reference pixels: left, top, and top-left. It might look like this, where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt; is the current pixel:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;C&lt;/span&gt;  &lt;span class=&quot;nx&quot;&gt;B&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;A&lt;/span&gt;  &lt;span class=&quot;nx&quot;&gt;D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s look at a Paeth function in isolation:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// paeth takes in each of our reference values&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;paeth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// first find P -- that is your comparison&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// now that we have p as a comparison, we can find which initial value is closest to p&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// take the absolute value of the difference between p and the current value&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// compare that difference to the previous difference&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// if the new difference is lesser -- use it&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;leastDiffObj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prev&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;curr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;curr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prev&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;curr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffObj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;prev&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;curr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;difference&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;prev&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffObj&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;difference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;leastDiffObj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; as a comparision value for the rest of the reference values. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; takes on the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a+b-c&lt;/code&gt;. We loop over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; to find the value that is furthest from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;, but note that we want the outcome to be less than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;. So given &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p=30&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a=20&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b=20&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c=65&lt;/code&gt;, even though &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; is furthest from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; would still win because it is a lesser value.&lt;/p&gt;

&lt;p&gt;Now that we’ve completed the paeth function, we can apply its result.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;  /* ... */

  switch(filterType) {
    /* ... */

    case 4:
+   let unfilteredPixels = []

+   currentRow.forEach((value, i) =&amp;gt; {
+     if (i === 0) {
+       const b = previousRow[i]

+       unfilteredPixels.push((value + b) % 256)

+       return
+     }

+     const a = unfilteredPixels[unfilteredPixels.length - 1] || 0
+     const b = previousRow[i] || 0
+     const c = previousRow[i - 1] || 0

+     let difference = paeth(a, b, c)

+     unfilteredPixels.push((value + difference) % 256)
+   })

+   return unfilteredPixels

    /* ... */
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Similar to cases 1 and 3, if we are on the first index of a row, that index has neither a left-hand neighbor, or a top-left neighbor, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; wins by default. Continuing through the loop, we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paeth&lt;/code&gt; function to determine the winning value, and then apply that value to the current pixel, exactly like the rest of the filter types. (Note: the value being applied as a result of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paeth&lt;/code&gt; function is the absolute value of the winning pixel, &lt;em&gt;not&lt;/em&gt; the difference between that value and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Now that we’ve made it to the end of our filter type method, we have a multidimensional array of 8-bit numbers, representing grayscale pixels! 🙌 What do we do with it? The sky’s the limit!&lt;/p&gt;

&lt;p&gt;If you’d like to see your progress thus far you could:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;paste the result into the terminal&lt;/li&gt;
  &lt;li&gt;use a PNG library to rewrite your pixels into a new PNG (or write your own 👀)&lt;/li&gt;
  &lt;li&gt;use the pixels to create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pgm&lt;/code&gt; file (&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/testPngDecoding.js#L18&quot;&gt;that’s what I did here&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned a lot in this project, and I really enjoyed recounting the journey through these posts. The project was a challenge, but it was a blast! I hope you found these posts helpful and informative, and I really appreciate you for reading them. Thank you!&lt;/p&gt;

&lt;p&gt;If you enjoyed this post, please check back soon. I’m still tinkering with PNGs, and this time I am implementing different image resizing algorithms and posting about my progress along the way.&lt;/p&gt;
</description>
        <pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate>
        <link>carmalou.com/image-manipulation-series/2025/09/04/parsing-pngs-pt-4.html</link>
        <guid isPermaLink="true">carmalou.com/image-manipulation-series/2025/09/04/parsing-pngs-pt-4.html</guid>
        
        <category>File-parsing</category>
        
        <category>Node</category>
        
        <category>JavaScript</category>
        
        <category>Image-interpolation</category>
        
        
        <category>image-manipulation-series</category>
        
      </item>
    
      <item>
        <title>Parsing PNGs with Node, Part 3</title>
        <description>&lt;p&gt;We left off &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/08/05/parsing-pngs-pt-2.html&quot;&gt;the last post&lt;/a&gt; with the image metadata and raw bytes for the pixels.&lt;/p&gt;

&lt;p&gt;Here’s the image I’m working against:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/carmalou/image-scaling-exercise/refs/heads/main/sample_images/grayscale-parrot.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/blog_examples/loopingThePixelArray.js&quot;&gt;And here’s the final output of this post.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk fully parsed provides this information:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;bitDepth&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;interlace&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our new function will take in the metadata and raw pixels like so:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;colorChannelMatrix&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;parsePixels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pixels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitDepth&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// the product of the colorType and bitDepth determines the bits per pixel&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerPixel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;colorChannelMatrix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitDepth&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// using the derived bits per pixel * width, find the bits per row / scanline&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerPixel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// finally divide that by 8 to find the bytes per row&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bytesPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bitsPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Before we get too far into parsing the pixels, let’s go over the image metadata, the raw pixels, and what it all signifies.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pixels&lt;/code&gt; parameter is a Buffer array, but let’s think about what that means. The buffer is a just a blob of pixels, and just like the chunks of the pixels, nothing signifies the beginning or end of a row. So how do we know when one row of pixels ends and another begins? Let’s get into it!&lt;/p&gt;

&lt;p&gt;Let’s start by considering the difference between a &lt;em&gt;single value&lt;/em&gt; versus a &lt;em&gt;single pixel&lt;/em&gt;. Consider RBG values, such as a baby blue &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;137, 207, 240&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/blue.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In order to apply that blue, we need all three of the numbers in the correct order. After all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;240, 137, 207&lt;/code&gt; is compromised of the same values, but is a completely different color!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pink.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s probably obvious that the individual values are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;137&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;207&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;240&lt;/code&gt;, but consider that the binary representation of this color looks like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And that’s just a single pixel! We talked before about how the byte array of pixels is just a flat blob of bits. Take a high definition photo, it might be 1920 x 1080 pixels or ~2.1 million pixels. Think about that compared to the single pixel array up above, because that’s a lot of data without an obvious place to start or stop! It’s easy to see how getting off by 1 or 2 bits could really change the outcome of our image.&lt;/p&gt;

&lt;p&gt;Fortunately, PNGs provide us with everything we need to pull pixels out of this haystack of 0s and 1s. To derive the number of bits in a single pixel, we combine &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bitDepth&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;colorType&lt;/code&gt;. There are 5 different color types, but for the purposes of this blog post, we’ll be focusing on grayscale. (&lt;a href=&quot;https://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.5&quot;&gt;Read more about different color types and their associated bit depths in the spec.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Color Type&lt;/code&gt; will only support certain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bit Depths&lt;/code&gt;. Grayscale is represented by two color type codes: 0 and 4. 0 indicates grayscale without transparency (alpha), and 4 indicates grayscale &lt;em&gt;with&lt;/em&gt; transparency. Color type 4 supports bit depths of 8 and 16, while Color Type 0 supports all bit depths (1, 2, 4, 8, and 16).&lt;/p&gt;

&lt;p&gt;You might be thinking, “ok, that’s interesting, but &lt;em&gt;what does it mean&lt;/em&gt;?” Essentially, your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;colorType&lt;/code&gt; will tell you how many &lt;em&gt;individual values&lt;/em&gt; make up a pixel, and your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bitDepth&lt;/code&gt; will tell you how many bits make up an individual value.&lt;/p&gt;

&lt;p&gt;So using my sample photo as an example:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerPixel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;colorChannelMatrix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitDepth&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1 * 8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For my image, the color type is 0, which indicates a single numerical value represents each pixel. The bit depth is 8, which indicates we need to parse 8 bits at a time to find the value for each pixel. Now we will use width to determine how many bits are in a row, and then convert that to bytes.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitsPerPixel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 8 * 150&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bytesPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bitsPerRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1200 / 8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Remember, while the sample image allocates 8 bits per pixel, or exactly one byte, it’s possible for a pixel to be larger or even smaller than a single byte. That’s why we first calculate the pixel size &lt;em&gt;in bits&lt;/em&gt; and then convert to bytes.&lt;/p&gt;

&lt;p&gt;Now that we know how many bytes are in a row, let’s continue:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;function parsePixels(signature, pixels) {
  const { height, width, colorType, bitDepth } = signature

  // the product of the colorType and bitDepth determines the bits per pixel
  let bitsPerPixel = colorChannelMatrix[colorType] * bitDepth

  // using the derived bits per pixel * width, find the bits per row / scanline
  let bitsPerRow = bitsPerPixel * width

  // finally divide that by 8 to find the bytes per row
  let bytesPerRow = Math.ceil(bitsPerRow / 8)

+ const numScanlines = height
+ let currentScanline = 0
+ const scanlineLength = bytesPerRow + 1 // add one for the filterByte

  // a matrix containing pixel values per row
+ const pixelMap = []

+ while (currentScanline &amp;lt; numScanlines) { /* ... */}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This sets us up to loop over the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pixels&lt;/code&gt; buffer array, knowing where a given row of pixels ends, and how many totals rows to expect. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scanlineLength&lt;/code&gt; is the number of bytes per row plus one. This is because the first pixel of a row is allocated for the filter type. This was my favorite part of PNGs! I think filter types are so clever, and deserve their own post, so we’ll cover them next.&lt;/p&gt;

&lt;p&gt;Let’s continue on the loop:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;function parsePixels(signature, pixels) {
  const { height, width, colorType, bitDepth } = signature

  // the product of the colorType and bitDepth determines the bits per pixel
  let bitsPerPixel = colorChannelMatrix[colorType] * bitDepth

  // using the derived bits per pixel * width, find the bits per row / scanline
  let bitsPerRow = bitsPerPixel * width

  // finally divide that by 8 to find the bytes per row
  let bytesPerRow = Math.ceil(bitsPerRow / 8)

  const numScanlines = height
  let currentScanline = 0
  const scanlineLength = bytesPerRow + 1 // add one for the filterByte

  // a matrix containing pixel values per row
  const pixelMap = []

  while (currentScanline &amp;lt; numScanlines) {
    // keep reference to initial offset
+   let startingOffset = currentScanline

    // increment row offset
+   currentScanline++

    // slice the scanline to get the current row&apos;s values
+   let row = pixels.subarray(startingOffset, startingOffset + scanlineLength)
+   let filteredRow = []

    // find the filter type and set the offset
+   let filterType = row[0];

    // set the offset to 1 to skip the filter type value
+   let offset = 1;

    // loop over the row itself
+   while (offset &amp;lt; row.length) {
+     let i = offset;

      // log out the current pixel value
+     console.log(row[i])

+     offset++;
+   }

    return {
      signature,
      pixelMap,
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let’s recap. At the beginning of this post we had a flat byte array and the PNG’s signature. We used the signature to derive the number of bits per pixel, and how many individual numbers make up the pixel. We also determined how many pixels are in a row, and how many rows make up the image. With all of this information, we’re now looping over the image accessing individual pixel values - wow!&lt;/p&gt;

&lt;p&gt;Are we done? If we take these pixel values and dump them into another file, will we be duplicating the PNG? Maybe, but probably not. There’s one more step, and that’s to apply the filter type. &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/09/04/parsing-pngs-pt-4.html&quot;&gt;I’ll cover that in the next post.&lt;/a&gt;&lt;/p&gt;

</description>
        <pubDate>Wed, 03 Sep 2025 00:00:00 +0000</pubDate>
        <link>carmalou.com/image-manipulation-series/2025/09/03/parsing-pngs-pt-3.html</link>
        <guid isPermaLink="true">carmalou.com/image-manipulation-series/2025/09/03/parsing-pngs-pt-3.html</guid>
        
        <category>File-parsing</category>
        
        <category>Node</category>
        
        <category>JavaScript</category>
        
        <category>Image-interpolation</category>
        
        
        <category>image-manipulation-series</category>
        
      </item>
    
      <item>
        <title>Parsing PNGs with Node, Part 2</title>
        <description>&lt;p&gt;We left off &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/08/05/parsing-pngs-pt-1.html&quot;&gt;the last post&lt;/a&gt; having successfully parsed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk, revealing metadata about the image.&lt;/p&gt;

&lt;p&gt;Here’s the image I’m working against:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/carmalou/image-scaling-exercise/refs/heads/main/sample_images/grayscale-parrot.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/blog_examples/accessingThePixels.js&quot;&gt;And here’s the final output of this post.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk fully parsed provides this information:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;bitDepth&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;interlace&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now that we have the image metadata, let’s parse the rest of the image!&lt;/p&gt;

&lt;p&gt;We’ll start by adding to our loop:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;function parsePng(data) {
  let width;
  let height;
  let bitDepth;
  let colorType;
  let compression;
  let filter;
  let interlace;

  // this will hold the pixel bytes as we pull them from the image
+ let idatBuffers = []

  // first handle for the signature
  let i = 8;
+ let futureBufLength = 0

  // loop over the data
  while (i &amp;lt; data.length) {
    // find the length of the current chunk
    let chunkLength = data.readUInt32BE(i);

    // find the type of the current chunk
    let chunkName = data.toString(&quot;ascii&quot;, i + 4, i + 8);

+   let chunkData = data.subarray(i + 8, i + 8 + chunkLength)

    // access IHDR
    if (chunkName == &quot;IHDR&quot;) {
      // create an offset to set the position at the beginning of the data section
      let ihdr = i + 8;

      // parse the header data
      width = data.readUInt32BE(ihdr);
      height = data.readUInt32BE(ihdr + 4);
      bitDepth = data[ihdr + 8];
      colorType = data[ihdr + 9];
      compression = data[ihdr + 10];
      filter = data[ihdr + 11];
      interlace = data[ihdr + 12];
    }

    // get each chunk of pixels
+   if (chunkName == &apos;IDAT&apos;) {
+     idatBuffers.push(chunkData)
+     futureBufLength += chunkLength
+   }

    // update the offset
    i = i + chunkLength + 12;
  }

  // return the header data
  return {
    width,
    height,
    bitDepth,
    colorType,
    compression,
    filter,
    interlace,
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the last post, we talked about how there are no spaces between chunks, so updating the offset will bring us to the beginning of the next chunk. For us, the next chunk we care about is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt; chunk.&lt;/p&gt;

&lt;p&gt;There can be multiple image data (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt;) chunks, and these chunks take on the structure of any other chunk. Their first four bytes will contain the length of the data section. The next four will indicate the chunk type. After the type, we’re finally accessing the actual data, phew!&lt;/p&gt;

&lt;p&gt;Within our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt; if-block, it’s pretty straightforward. We use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chunkData&lt;/code&gt;, which is a Buffer array containing only the data, and we push that into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idatBuffers&lt;/code&gt; which will be our reference to our pixels.&lt;/p&gt;

&lt;p&gt;Now that we have our pixels in an array, we’re ready for the next step.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;const zlib = require(&apos;zlib&apos;)

function parsePng(data) {
  let width
  let height
  let bitDepth
  let colorType
  let compression
  let filter
  let interlace

  // this will hold the pixel bytes as we pull them from the image
  let idatBuffers = []

  // loop over the data
  // first handle for the signature
  let i = 8
  let futureBufLength = 0

  while (i &amp;lt; data.length) {
    let chunkLength = data.readUInt32BE(i)
    let chunkName = data.toString(&apos;ascii&apos;, i + 4, i + 8)
    let chunkData = data.subarray(i + 8, i + 8 + chunkLength)

    // get and set the signature
    if (chunkName == &apos;IHDR&apos;) {
      let ihdr = i + 8
      width = data.readUInt32BE(ihdr)
      height = data.readUInt32BE(ihdr + 4)
      bitDepth = data[ihdr + 8]
      colorType = data[ihdr + 9]
      compression = data[ihdr + 10]
      filter = data[ihdr + 11]
      interlace = data[ihdr + 12]
    }

    // get each chunk of pixels
    if (chunkName == &apos;IDAT&apos;) {
      idatBuffers.push(chunkData)
      futureBufLength += chunkLength
    }

    // signifies the end of the file
    // since we are all the way through the file, return the signature and pixels
+   if (chunkName == &apos;IEND&apos;) {
+     const pixel = Buffer.concat(idatBuffers, futureBufLength)

+     try {
+       let decompressed = zlib.inflateSync(pixel)

+       return {
+         signature: {
+           width,
+           height,
+           bitDepth,
+           colorType,
+           compression,
+           filter,
+           interlace,
+         },
+         pixelBytes: decompressed,
+       }
+     } catch (err) {
+       console.error(&apos;Failed to decompress:&apos;, err)
+       return
+     }
    }

    i = i + chunkLength + 12
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last chunk of a valid PNG will always be the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IEND&lt;/code&gt; chunk. Its data section will be empty, so once we find this chunk, we know we’re at the end of our file stream.&lt;/p&gt;

&lt;p&gt;A few things happen here:&lt;/p&gt;

&lt;p&gt;First, we create a new Buffer array with all of our pixel bytes (note that even though the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idatBuffer&lt;/code&gt; array was a nested array, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pixel&lt;/code&gt; array will be flat).&lt;/p&gt;

&lt;p&gt;That’s not the important part though - the most important part of this new set of code is here: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zlib.inflateSync(pixel)&lt;/code&gt;. PNGs compress their pixel data using zlib compression. You can find more information about this &lt;a href=&quot;https://www.libpng.org/pub/png/spec/1.2/PNG-Compression.html&quot;&gt;here in the spec.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re using the Node zlib library to “inflate” or decompress the pixel data. I plan to manually implement the algorithm in the future, but for now, we’ll just use the library.&lt;/p&gt;

&lt;p&gt;Now that we have the image metadata (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;signature&lt;/code&gt;) and the raw pixels – we’re ready to get started! &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/09/03/parsing-pngs-pt-3.html&quot;&gt;Check out the next blog post&lt;/a&gt; where we &lt;em&gt;finally&lt;/em&gt; start decoding these pixels!&lt;/p&gt;
</description>
        <pubDate>Thu, 07 Aug 2025 00:00:00 +0000</pubDate>
        <link>carmalou.com/image-manipulation-series/2025/08/07/parsing-pngs-pt-2.html</link>
        <guid isPermaLink="true">carmalou.com/image-manipulation-series/2025/08/07/parsing-pngs-pt-2.html</guid>
        
        <category>File-parsing</category>
        
        <category>Node</category>
        
        <category>JavaScript</category>
        
        <category>Image-interpolation</category>
        
        
        <category>image-manipulation-series</category>
        
      </item>
    
      <item>
        <title>Parsing PNGs with Node, Part 1</title>
        <description>&lt;p&gt;This blog post is the first in a series about image manipulation and parsing. This project started because I was curious how I might go about manually resizing an image. I’m also experimenting with manual image generation as a part of this project.&lt;/p&gt;

&lt;p&gt;If you’d like to follow along as we go, the repo for the project is &lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise&quot;&gt;here.&lt;/a&gt; The &lt;a href=&quot;https://www.libpng.org/pub/png/spec&quot;&gt;PNG spec is here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the image I used:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/carmalou/image-scaling-exercise/refs/heads/main/sample_images/grayscale-parrot.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.libpng.org/pub/png/pngsuite.html&quot;&gt;libpng also has a comprehensive suite of images to use in tests.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s jump in.&lt;/p&gt;

&lt;h2 id=&quot;the-question-what-do-the-internals-of-an-image-actually-look-like&quot;&gt;The question: What do the internals of an image actually look like?&lt;/h2&gt;

&lt;p&gt;It started with curiosity around this question: “What &lt;em&gt;actually is&lt;/em&gt; an image? What does it look like under the hood?” Obviously different image formats will look different interally, so I had to pick one to start with, and I chose PNGs. Why PNGs? I didn’t have any particular reasoning behind selecting PNGs as my test case. Since I work on a Mac, I thought being able to generate my own PNG samples via screenshots might make this easier. That turned out not to be the case, but more on that later.&lt;/p&gt;

&lt;p&gt;I knew there was only one to find out – crack open the file and look inside.&lt;/p&gt;

&lt;h2 id=&quot;png-internals&quot;&gt;PNG Internals&lt;/h2&gt;

&lt;p&gt;While there are many different chunk types defined for a PNG, only three are absolutely required for a valid PNG. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk, at least one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt; chunk (although there could be multiple, depending on the size of the image), and lastly the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IEND&lt;/code&gt; chunk.&lt;/p&gt;

&lt;p&gt;Because the goal of my project was not to write a brand new PNG viewer (although, that could happen in a future post!), I focused only on the absolutely necessary parts. As such, this post will not cover chunk types outside of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IEND&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note: If you’d like to learn more, all chunk types are covered &lt;a href=&quot;https://www.libpng.org/pub/png/spec&quot;&gt;in the spec&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;ihdr-values-and-structure&quot;&gt;IHDR Values and Structure&lt;/h3&gt;

&lt;p&gt;PNGs are made up of multiple different “chunks”. In order to be a valid PNG, the first chunk must be the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk. The Image Header chunk (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt;) contains, you guessed it, information about the image. It is always 13 bytes and contains the following information, which will always be in this order:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Width (in pixels)&lt;/li&gt;
  &lt;li&gt;Height (in pixels)&lt;/li&gt;
  &lt;li&gt;Bit depth&lt;/li&gt;
  &lt;li&gt;Color type&lt;/li&gt;
  &lt;li&gt;Compression method&lt;/li&gt;
  &lt;li&gt;Filter method&lt;/li&gt;
  &lt;li&gt;Interlace method&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll explain more about these in the next post. For now, just know that since the image is a flat array of bytes, we need all of this information to derive information such as: where a row of pixels starts and stops, how many bits or bytes make up a single pixel, how many color channels are used, and more. For now, we’ll focus on parsing out this information.&lt;/p&gt;

&lt;h3 id=&quot;parsing-the-ihdr&quot;&gt;Parsing the IHDR&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/carmalou/image-scaling-exercise/blob/main/blog_examples/parsingTheHeader.js&quot;&gt;Here is an example from my project to parse the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we need to handle for the PNG signature. The signature is always 8 bytes (bytes, not bits), and contains the same decimal values. These values indicate the file type is a PNG. Since we know we’re parsing a PNG, we’ll just skip the signature.&lt;/p&gt;

&lt;p&gt;Our method will take the buffer response from reading the file as it’s parameter.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;parsePng&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// the variables we are looking for from the IHDR chunk&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bitDepth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interlace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// first handle for the signature&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So far, it’s pretty straightforward. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; parameter is a buffer (an array of bytes). After we instantiate the variables we plan to parse out of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt;, we create our offset, which skips the first 8 bytes. Next we would start looping over the buffer, but before we get too far into that, let’s talk about chunk structure.&lt;/p&gt;

&lt;p&gt;In the first section, I mentioned that the PNG buffer is a flat array of bytes - that means there are no delimiters to signal the end of a line or the beginning of the next line. There’s also no delimiter between chunks. In fact, there are no spaces or empty bytes at all between any of the sections of a PNG. However, it’s possible to derive when you’re at the end of a chunk, based on the structure.&lt;/p&gt;

&lt;p&gt;A chunk is made up of four parts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Length&lt;/strong&gt; - the first 4 bytes of a chunk indicate the length of the data section. It’s important to note that this length is &lt;em&gt;not&lt;/em&gt; the length of the current chunk. It is only representative of the data within the chunk.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Type&lt;/strong&gt; - the next 4 bytes tell you the type (name) of the current chunk&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt; - this is the part we are really after! This is the only part of a chunk with a dynamic length. All other sections are 4 bytes long.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CRC&lt;/strong&gt; - the final 4 bytes of a chunk. It’s essentially a checksum to validate your offsets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every single chunk type will follow this structure and order, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDAT&lt;/code&gt; chunks.&lt;/p&gt;

&lt;p&gt;Now that we know a little more about what we’re looking at, let’s continue:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;function parsePng(data) {
  let width;
  let height;
  let bitDepth;
  let colorType;
  let compression;
  let filter;
  let interlace;

  // first handle for the signature
  let i = 8;

  // loop over the data
+  while (i &amp;lt; data.length) {
    // find the length of the current chunk
+    let chunkDataLength = data.readUInt32BE(i);

    // update the offset
+    i = i + chunkLength + 12;
+  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’ve added the loop and the first building blocks we need. In the loop, we first find the length of the data section, and then we use that to update the loop’s offset. We take the current position in the buffer, and add the data length (because this is the part of a chunk whose length is dynamic) + 12 bytes, to cover the rest of the chunk. Remember, there are no spaces or empty bytes between chunks, so this offset will take us to the very first byte of the following chunk.&lt;/p&gt;

&lt;p&gt;Now we’re ready to parse the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-diff-javascript&quot;&gt;function parsePng(data) {
  let width;
  let height;
  let bitDepth;
  let colorType;
  let compression;
  let filter;
  let interlace;

  // first handle for the signature
  let i = 8;

  // loop over the data
  while (i &amp;lt; data.length) {
    // find the length of the current chunk
    let chunkLength = data.readUInt32BE(i);

    // find the type of the current chunk
+   let chunkName = data.toString(&quot;ascii&quot;, i + 4, i + 8);

    // access IHDR
+   if (chunkName == &quot;IHDR&quot;) {
      // create an offset to set the position at the beginning of the data section
+     let ihdr = i + 8;

      // parse the header data
+     width = data.readUInt32BE(ihdr);
+     height = data.readUInt32BE(ihdr + 4);
+     bitDepth = data[ihdr + 8];
+     colorType = data[ihdr + 9];
+     compression = data[ihdr + 10];
+     filter = data[ihdr + 11];
+     interlace = data[ihdr + 12];
+   }

    // update the offset
    i = i + chunkLength + 12;
  }

  // return the header data
+ return {
+   width,
+   height,
+   bitDepth,
+   colorType,
+   compression,
+   filter,
+   interlace,
+ };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You might notice a difference between the way we treat the first value within a chunk (length) and the next value (type). The length is always 32-bit integer, whereas the chunk type is always 4 ASCII characters. Because these are different data types, we must access their values differently.&lt;/p&gt;

&lt;p&gt;The next bit of code is very straightforward. We identify the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IHDR&lt;/code&gt; chunk and set the header values. Notice again that the first two values are treated differently than the next five. Because those first two initial values are 32-bit integers, we need to parse all four bytes together to find the correct value for each.&lt;/p&gt;

&lt;p&gt;The final five values are all single byte values, so they can be directly accessed.&lt;/p&gt;

&lt;p&gt;The image header for the test image I used looked like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;bitDepth&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;colorType&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;interlace&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now that we have the image header information, we’re ready to get into the actual pixel values of the PNG! I’ll cover that &lt;a href=&quot;https://carmalou.com/image-manipulation-series/2025/08/07/parsing-pngs-pt-2.html&quot;&gt;in the next blog post&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate>
        <link>carmalou.com/image-manipulation-series/2025/08/05/parsing-pngs-pt-1.html</link>
        <guid isPermaLink="true">carmalou.com/image-manipulation-series/2025/08/05/parsing-pngs-pt-1.html</guid>
        
        <category>File-parsing</category>
        
        <category>Node</category>
        
        <category>JavaScript</category>
        
        <category>Image-interpolation</category>
        
        
        <category>image-manipulation-series</category>
        
      </item>
    
      <item>
        <title>Migrate Data In Postgres</title>
        <description>&lt;p&gt;Lately I’ve been doing a lot more data-driven work, including data analysis. From this work, I’ve needed to import and export &lt;em&gt;a lot&lt;/em&gt; of data. Here is my best quick-and-dirty way to copy a table from one Postgres table to another. I frequently use this method to refresh my local environment with production data.&lt;/p&gt;

&lt;p&gt;Of course, if you’re using a tool like &lt;a href=&quot;https://www.pgadmin.org/download/pgadmin-4-macos/&quot;&gt;pgAdmin&lt;/a&gt;, you can export data at the click of a button, but depending on what you plan to do with the data, that may not be what you want to do. Using the pgAdmin export will include all of the record IDs, so if you’re using this data to augment another database (or in my case, refresh), this might not be what you want. But if that doesn’t matter, using a built-in import/export tool is definitely the easiest solution.&lt;/p&gt;

&lt;p&gt;Unfortunately this usually doesn’t work for me, so this is what I do instead:&lt;/p&gt;

&lt;h3 id=&quot;export-the-prod-data-into-a-csv&quot;&gt;&lt;strong&gt;Export the prod data into a CSV&lt;/strong&gt;&lt;/h3&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;copy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnA&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnC&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tablename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;absolute/path/plus/file.csv&apos;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;csv&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DELIMITER&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Make sure you include the absolute filepath, otherwise you’ll have a lot of trouble finding your csv. (This is the absolute path to &lt;em&gt;your&lt;/em&gt; local machine.)&lt;/p&gt;

&lt;p&gt;You can include any query above, so if you need to filter or sort in a certain way, adjust your query accordingly.&lt;/p&gt;

&lt;h3 id=&quot;drop-the-data-from-my-local-database-and-reset-the-sequence&quot;&gt;&lt;strong&gt;Drop the data from my local database and reset the sequence&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Make &lt;strong&gt;triple-sure&lt;/strong&gt; you’re in the correct environment before you do this!!&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;truncate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tablename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nextval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;sequencenameforprimarykey&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sequence&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sequencename&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;restart&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nextval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;sequencenameforprimarykey&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tablename&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After you truncate your table and reset your sequence, you can select each and see that you’re ready to start fresh with your prod data.&lt;/p&gt;

&lt;h3 id=&quot;insert-data-from-the-csv&quot;&gt;&lt;strong&gt;Insert data from the CSV&lt;/strong&gt;&lt;/h3&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;copy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columnA&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;absolute/path/plus/file.csv&apos;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;csv&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DELIMITER&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice that the above command is basically just the inverse of the first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy&lt;/code&gt; command in step 1.&lt;/p&gt;

&lt;p&gt;Depending on how much data you’re copying over, this step might take a while. At the end, you’ll see something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY 137&lt;/code&gt; in your terminal, which indicates the copy is complete and 137 records were copied into your table.&lt;/p&gt;

&lt;p&gt;I’ll usually use one more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select count(*) from tablename&lt;/code&gt; to see the total number of records in the table, as a sanity check.&lt;/p&gt;

&lt;h3 id=&quot;a-few-gotchas-to-watch-for&quot;&gt;A few gotchas to watch for:&lt;/h3&gt;

&lt;h4 id=&quot;permission-denied-errors&quot;&gt;&lt;strong&gt;Permission denied errors&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;If you used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\COPY&lt;/code&gt; without root permissions, try again with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\COPY&lt;/code&gt; command has a slightly different signature and requires root access, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy&lt;/code&gt; will work regardless.&lt;/p&gt;

&lt;h4 id=&quot;cannot-insert-type-into-column&quot;&gt;&lt;strong&gt;“Cannot insert {type} into column”&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;An issue of incorrect data to column type. When this happens to me, it usually means I forgot to specify the columns in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy to&lt;/code&gt; command, or I specified them in the wrong order. If you are going to ignore the IDs when you move data from one place to another – which is a good idea – you must specify the columns in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy to&lt;/code&gt; command!&lt;/p&gt;

&lt;h4 id=&quot;missing-data-for-column&quot;&gt;&lt;strong&gt;“Missing data for column”&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;When this happens to me, it usually means I forgot to specify the delimiter to be a comma in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\copy to&lt;/code&gt; command. Check your CSV file and ensure that you’re using the correct delimiter for your data.&lt;/p&gt;

&lt;h4 id=&quot;duplicate-key-value-violates-unique-constraint&quot;&gt;&lt;strong&gt;“Duplicate key value violates unique constraint”&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;Most likely cause is that you copied your entire table, including ids, but forgot to drop your secondary table’s data and/or reset the sequence.&lt;/p&gt;

&lt;p&gt;And that’s about it! At this point, I’ve copied production data into a local or sandbox environment where its safe to tinker with or even delete data. Just &lt;strong&gt;be careful&lt;/strong&gt; and always, always, always make sure you’re not in production when tinkering!&lt;/p&gt;

&lt;p&gt;Happy SQL-ing!&lt;/p&gt;
</description>
        <pubDate>Wed, 23 Apr 2025 00:00:00 +0000</pubDate>
        <link>carmalou.com/2025/04/23/migrate-data-in-postgres.html</link>
        <guid isPermaLink="true">carmalou.com/2025/04/23/migrate-data-in-postgres.html</guid>
        
        
      </item>
    
      <item>
        <title>Shutting down my startup: The End of Neptune Divorce</title>
        <description>&lt;p&gt;It’s sort of hard to know where to start writing this blog post, especially since I’ve never blogged about this project before. I think the idea of “#BuildInPublic” where you share your progress along the way is neat, and its great for marketing, but, funny enough, its also a lot of work! When you’re trying to build a business - marketing, coding, networking, selling, and all that - it’s tough to find the time to craft a cute tweet about where you’re at.&lt;/p&gt;

&lt;p&gt;I’ll be honest here and add that, just like with all of social media, I think people share the happy bits, and skim over the hard parts. That mentality always made me a little hesitant to share, because my experience with Neptune was a lot more hard parts than happy parts.&lt;/p&gt;

&lt;p&gt;Tl;dr: Neptune Divorce is shutting down. The rest of this post is a chronicle of the journey I took building Neptune, and any nuggets of knowledge I might’ve gained along the way.&lt;/p&gt;

&lt;h3 id=&quot;neptune-the-beginning&quot;&gt;Neptune: the beginning&lt;/h3&gt;

&lt;p&gt;I &lt;a href=&quot;https://twitter.com/carmalou/status/1435016146130980873&quot;&gt;first tweeted about Neptune Divorce&lt;/a&gt; a little over a year ago when we had our launch on Twitter. My business partner and I had been slowly working on it for a long time. For me, it all started years ago when I went through a divorce myself and I couldn’t believe that there wasn’t some sort of online service to handle all of this for you. I thought at the time that I had some sort of brilliant revolutionary idea, but, spoiler alert, today I am not so sure.&lt;/p&gt;

&lt;p&gt;Neptune, in a nutshell, was a way for someone to go online, answer a few questions, and get divorce paperwork. The entire process could be completed in less than an hour. I knew from my own experience how difficult it was to find someone both affordable &lt;em&gt;and&lt;/em&gt; reliable to prepare divorce paperwork. And, of course, the last thing you want to think about when your life has been turned upside down is whether or not the person who is supposed to help you is actually just bullshitting you.&lt;/p&gt;

&lt;p&gt;I thought this beautiful little app could alleviate at least some of the stress a person goes through when they are getting divorced. We wanted to make it as easy as possible, so creating an account was free, and using the app was free. You paid at the end, once you were ready to actually get your documents. My partner and I hoped that if we left payment until the end, we would be able to build a trust relationship with our customers.&lt;/p&gt;

&lt;p&gt;Neptune germinated in my brain for a few years before anything came to fruition. I talked endlessly about the concept, built a small prototype, and generally thought I had a winning idea. But, at the same time, I was too scared to launch a product like this without a business partner, and more specifically a legal business partner. Finding the right business partner was tricky for me. As a woman in tech, I was wary of partnering with the right person who was going to listen to me and let me do what I do best. Almost every day, I deal with dudes in tech who don’t take me seriously, so the very last thing I wanted was something like that in my own business. I’m thankful to say that connecting with my business partner was likely the best thing to come out of this whole adventure. She’s an absolute beast in her field, and at the same time, she recognizes that I am an expert in mine. We complement each other, and working with her has been an absolute blast.&lt;/p&gt;

&lt;h3 id=&quot;cheerleaders-pitches-and-accelerators&quot;&gt;Cheerleaders, pitches, and accelerators&lt;/h3&gt;

&lt;p&gt;One person I want to call out (in a positive way) is &lt;a href=&quot;https://twitter.com/kristinsooner&quot;&gt;Kristin Garcia&lt;/a&gt;. She is truly a gem, and was our biggest cheerleader along the way. Kristin and I met through &lt;a href=&quot;https://twitter.com/techlahoma&quot;&gt;Techlahoma&lt;/a&gt;, and I can’t say enough kind things about her. When I shared this project with her, I was still very shy about talking about it, but Kristin saw the potential before I did.&lt;/p&gt;

&lt;p&gt;Kristin pushed us to do two really important things: complete a pitch competition and apply for an accelerator. Even though Neptune is at an end, those were really cool things to be a part of.&lt;/p&gt;

&lt;h4 id=&quot;the-pitch-competition&quot;&gt;The pitch competition&lt;/h4&gt;

&lt;p&gt;Kristin reached out about a pitch competition in Tulsa. The only downside was that we didn’t have a pitch deck, and the competition was only days away. Once we knew we would be pitching, we spent every free second practicing and working on our pitch deck.&lt;/p&gt;

&lt;p&gt;The competition was specifically for female founders and, let me say, there’s nothing like being in a room with a bunch of supportive women doing really amazing things. Every business there deserved to take first place, and even though there were only two prizes, all of these ladies remained so supportive of each other. I don’t have words for how amazing it was. It will always be one of my favorite experiences of the whole Neptune rollercoaster.&lt;/p&gt;

&lt;p&gt;Unfortunately since we had so little notice, Harmonniey, my business partner, wasn’t able to join. This meant I had to pitch, which is not really what I wanted my role to be in the business. I would’ve been happy to stay behind my laptop building, and let Harmonniey do all the talking. But if we wanted to participate in the competition, it was going to have to be me.&lt;/p&gt;

&lt;p&gt;On the day of the competition, my sister and I hopped in the car and drove nearly 2 hours to Tulsa. At this point, I was still really nervous sharing Neptune, so I practiced my pitch all the way there. As I sat in the audience listening to all these incredible women pitching their businesses, it was clear that all of us were all trying to use our skills to make positive change in the world.&lt;/p&gt;

&lt;p&gt;After the pitches were complete, the judges disappeared to discuss who would win the prizes, and the founders had time to chat with the audience. I had so many people approach me to congratulate me and tell me how incredibly needed this product was. Everyone had a story about a friend, aunt, brother, or cousin who had the hardest time navigating the divorce process and how this app would’ve been so helpful for them. One woman told me that Neptune would be “generation-changing.” I still think about that sometimes. It was so much validation and exactly at a time when we needed it. We had done a soft-launch the previous September, and since neither myself or Harmonniey really knew how to market, nothing had happened. I tweeted about it once, and then got a few positive responses from friends, but nothing more. Here we were in March, a full six months later, with no customers yet, but hearing this positive feedback made me sure that we &lt;em&gt;would&lt;/em&gt; get customers, if we could only be patient.&lt;/p&gt;

&lt;p&gt;When the judges returned to announce the winners, they said what everyone in the room was thinking - it was an incredibly hard decision. Everyone who presented had a great idea, and they wished that they could give everyone a prize. They announced first place, and then they announced second place - no Neptune. Of course, I was disappointed, but it was hard to be too disappointed after such great feedback from the crowd, and when the winners were so deserving. But then - I’ll never forget this - completely out of left field, they announced a surprise third place prize and it was for Neptune. Talk about validation - the crowd telling me how great an idea this was, and then the judges saying that they added an extra prize and it was going to Neptune?! Surely, &lt;em&gt;surely&lt;/em&gt; this idea would work. All we needed was time. (As a side note, having my sister there with me for that was amazing. I’m not sure I would’ve even believed it had she not been there too.)&lt;/p&gt;

&lt;h4 id=&quot;the-accelerator&quot;&gt;The accelerator&lt;/h4&gt;

&lt;p&gt;Not long after the pitch competition, Kristin struck again! She knew of an accelerator and she thought we should apply. I’ll admit, I wasn’t sure. Long before Harmonniey had come onboard, back when Neptune was little more than an idea and a prototype, I had applied to an accelerator as a solo founder, and I didn’t get in. That stuck with me, and because of it, I was pretty sure we wouldn’t get in. But with Kristin’s encouragement, we did  but Kristin really encouraged us to try, and so Harmonniey and I took the leap. We got in!&lt;/p&gt;

&lt;p&gt;The accelerator was &lt;em&gt;intense.&lt;/em&gt; I already thought about Neptune all the time, but this turned it up to 11. Each week we would meet with different mentors - business owners, marketers, venture capitalists, and all kinds of people each with varying disciplines. And each week we would explain that, no, we didn’t have customers, but it was a solid idea, and we knew it could work. Each week, we asked the same question: how do you market something like this? It isn’t a purse, it isn’t an entertainment app. Getting divorced is a very private, very painful thing, Even if you’re the person initiating, it is still turning your whole life upside-down. How do you sell something like that? Targeted ads? Well, most people don’t hop on social media and share their exciting divorce news. Even though we kept telling people we didn’t have customers yet, we tried to remain upbeat. And our mentors were mostly upbeat as well. Most of them were excited about the idea and they thought it was really innovative and needed.&lt;/p&gt;

&lt;p&gt;The accelerator was very focused on getting a business funded through VCs, and even though Harmonniey and I weren’t sure about taking on funding, working on a pitch was a big part of the cirriculum. We leaned in and practiced our pitch, and tried to gloss over the fact that we didn’t have any paying customers. After all, the numbers appeared to be on our side. The divorce industry in the U.S. is a billion dollar a year industry. Oklahoma, our homebase, has the second highest divorce rate in the country. If we could capture as low as 5 percent of the Oklahoma market, that would be a sustainable business. If we could duplicate that in other states with high populations, like Texas, we were talking about millions of dollars per year.&lt;/p&gt;

&lt;p&gt;As practice leading up to the big pitch night at the end of the accelerator, we had “Investor week” where we practice-pitched to investors. This coincided with a vacation I had planned, so I took my laptop on vacation and pitched investors from my hotel room, and tried to figure out new and different ways to spin a lack of customers. Investor week went surprisingly well considering our customer issues. And even better, during investor week, Harmonniey and I finally pulled the trigger on paying for Google ads. And if that wasn’t enough, during this vacation, I learned that we had our first two real sign ups on our website. Let me tell you - I was elated. I texted Harmonniey at 11pm to tell her the great news. It was really happening! Our hard work was finally paying off, and Neptune was going to work. I could feel it.&lt;/p&gt;

&lt;h3 id=&quot;believing-in-the-mission&quot;&gt;Believing in the mission&lt;/h3&gt;

&lt;p&gt;Any article from Insider or Forbes will tell you that you have to be your business’ biggest cheerleader. Of course that is true, but at the same time, that’s not really my style. I’m naturally a very cautious person. Some people have even called me pessimistic. And for a long time, that carried over to Neptune as well. I believed it &lt;em&gt;could&lt;/em&gt; work, but I wasn’t willing to bet the house on it. But at some point in all of this, I started drinking my own kool-aid and believing we had something really amazing on our hands. And part of me still believes that’s true.&lt;/p&gt;

&lt;p&gt;We had a socially-minded app, when it feels like most apps coming out of Silicon Valley are putting different kinds of mustaches on an image and saying that’s “changing the world.” We were two women building a tech company in Oklahoma, one of the most conservative states in the nation. And, over time, we came to the realization that our app wasn’t just about making the divorce process easier, but an access-to-justice app. We started to really focus on the idea that our app was about making the justice system more accessible to individuals with lower incomes. And I really, really wish that had turned out to be the case. I think it would’ve been amazing to achieve something like that, but unfortunately the numbers got in the way.&lt;/p&gt;

&lt;h3 id=&quot;making-the-call-to-use-ads&quot;&gt;Making the call to use ads&lt;/h3&gt;

&lt;p&gt;Of course, through our time at the accelerator we had spoken to lots of different business owners, and a resounding piece of advice we got was &lt;strong&gt;don’t use ads&lt;/strong&gt;. Don’t pay for ads until you have to. And, to an extent, I do think this is a solid piece of advice. Ads are very expensive, and there are many free ways to market a business. Social media is an obvious one, but even simpler than that, just finding potential customers and talking to them. Yes, it’s uncomfortable, but it’s free.&lt;/p&gt;

&lt;p&gt;Marketing divorce, though, that is tricky. People don’t want to tell you they’re getting a divorce. Harmonniey and I talked about this every day for months - how do we market something that’s focused on one of the worst things a person can go through? Everything we read seemed to be focused on physical products, a specific niche within a business, or fun consumer-based apps. Every week we asked our different mentors about how to market something like this, and we scoured the internet searching for tips on something like we were building, and found almost nothing.&lt;/p&gt;

&lt;p&gt;After a while ads seemed like our only option. We went into it accepting that if we didn’t have any customers after 60 days, we would shut down ads and reassess. We initially tried running ads ourselves and found it very difficult. We weren’t getting the results we wanted, so we hired someone to help us. At this point, we were spending a significant amount of money each month on the business. We both knew this wasn’t something we could justify forever, but we both really believed that if we could only reach the right people, we would start getting paying customers, which would justify paying for the ads.&lt;/p&gt;

&lt;h3 id=&quot;pitch-night&quot;&gt;Pitch Night&lt;/h3&gt;

&lt;p&gt;As if it were some sort of teen sports movie, it had all led up to this: Pitch Night! Pitch night was open to the public and it was the place to showcase all we had learned and done during the accelerator.&lt;/p&gt;

&lt;p&gt;Harmonniey had encouraged me to do the pitch, since it was my initial idea, and my story behind the app. I agreed with her, but in my perfect world I would live behind the scenes and just build the app. Talking about Neptune meant talking about my divorce, and I always felt weird about that. But I understood what Harmonniey meant, and I practiced the pitch diligently.&lt;/p&gt;

&lt;p&gt;Even though we had some sign ups on our app, the ads had only been running a few weeks at this point, so we didn’t have any customers from ads yet. And unfortunately, none of the sign ups we did have had turned into paying customers. Just hours before the pitch, I received an email from a user I had been corresponding with where she shared that she had gone another direction due to the app being too buggy. It was a devastating blow just before the pitch. We resolved the bug really quickly, but it was too late for that customer.&lt;/p&gt;

&lt;p&gt;It was tough to get back in the mindset before the pitch, but with encouragement from Harmonniey and other friends from the accelerator, &lt;a href=&quot;https://twitter.com/susan_beth/status/1534324203226386434?s=20&amp;amp;t=5IULv9XBpBUgOZTRFfhqrg&quot;&gt;the pitch went really well.&lt;/a&gt; Lots of different people, including a handful of my friends, attended the pitch and we got more validation about how necessary something like this is. But at the same time, one person asked a haunting question: “with the numbers you described, if the app is as easy to use as you say, why isn’t everyone using it?” I felt dread even before they had finished asking the question, because I had been asking myself the same thing for months, and I couldn’t come up with an answer.&lt;/p&gt;

&lt;h3 id=&quot;going-downhill&quot;&gt;Going downhill&lt;/h3&gt;

&lt;p&gt;After the accelerator, we kept going with as much enthusiasm as we could, but it became more and more difficult without any customers. We had far more money going out than we were seeing coming in. After a few months like that, it was difficult to imagine that a few orders would come in and make up for the deficit, but still we pushed on.&lt;/p&gt;

&lt;p&gt;I made the call to lets ads go on for longer than planned because I really believed if we just had more time something would change. But after four months - twice as long as we had initially agreed - it was clear that Neptune wasn’t going to work. We had the data to show that people were in fact making it to our site, and some people were even signing up. But something was happening after sign up. People would simply drop off and never log in again. Any business advice you read about user acquistion tells you to reach out to your users actively for feedback. Give your product away for free in exchange for a 20 minute call for feedback, talk about your product all you can and accept all feedback.&lt;/p&gt;

&lt;p&gt;But when the product is divorce paperwork, its difficult to reach out and ask someone to share that experience in the name of feedback. And that’s assuming anyone responds at all. It’s already difficult to get customer feedback on surveys, but can you imagine if someone asked you to please share how your divorce went?&lt;/p&gt;

&lt;h3 id=&quot;shutting-down&quot;&gt;Shutting down&lt;/h3&gt;

&lt;p&gt;After four months of ads, the numbers were clear. Of the traffic that made it to the site, we did have some user conversions. But of those users, almost none actually used the app, and fewer still completed the process. No one completed the process and went on to pay for the completed paperwork and instructions. Four months of paying for ads, a year in business, and thousands of hours building, talking, thinking, and living Neptune, I had to accept that it just wasn’t going to work. Late last month, Harmonniey and I met at her office, exchanged very sad glances, and agreed that it was over.&lt;/p&gt;

&lt;h3 id=&quot;what-does-it-all-mean&quot;&gt;What does it all mean?&lt;/h3&gt;

&lt;p&gt;It’s difficult to know why users were dropping off after sign up, but we have some ideas. One of those ideas is that we have a trust issue with our customers. Obviously the divorce process is a very emotional time, whether you’re initiating it or not. One thing we heard a few times was “how does a person know this site is legit?” Of course we included all over the site that it was designed and created by an attorney, but trust is about more than the idea that the paperwork is correct. Going to court and filing a divorce petition is overwhelming. The trust relationship a person has with their attorney is about trusting that the attorney is writing up the correct paperwork, and it’s also about how that attorney makes their client &lt;em&gt;feel&lt;/em&gt;. Can an app replicate that sense of a security? I think some apps can for some people, but its clear to me now that Neptune did not.&lt;/p&gt;

&lt;p&gt;Another difficult item was pricing. There are businesses that provide this service at a much lower cost. We thought we could charge more for our service since it was created by an attorney, however a few times we got feedback from non-customers that the app was too expensive compared to our competitors. (A few times we even got feedback that it should be free!) This creates problems with the sustainability of our business. I wouldn’t say that Harmonniey and I were in this to make millions (although we wouldn’t have turned it down!), but we also didn’t want to offer it for free. After all, it’s a single-use product (you would hope anyway), so any time you drop the price, that dramatically affects your overall earnings. There are a limited amount of divorces we could hope to capture, so it’s not as though we would be able to make it up on volume either.&lt;/p&gt;

&lt;p&gt;Lastly, it takes a long time for a person to go from thinking about getting a divorce to being ready to actually file. We believe it’s very likely that a lot of the users we saw sign up will end up filing, but how long will it take? I’ve read estimates that say people take an average of four years to make the leap from thinking about divorce and deciding to do it, to actually filing and getting divorced. When we thought about that, it became even more difficult to keep going. We had ongoing costs each month for infrastructure alone, not even considering the ads we were having to pay for in order to get these sign ups. If we assume generously it takes a year for a person to sign up for our app and then convert to a paying customer, we have lost money on that customer without even accounting for ad spend. It was an untenable situation.&lt;/p&gt;

&lt;h3 id=&quot;couldnt-you-just-leave-it-going&quot;&gt;Couldn’t you just leave it going?&lt;/h3&gt;

&lt;p&gt;We did consider just turning off ads and leaving the app running. With ads turned off, it costs very little to actually run the site. I’m fairly certain we could go into the settings on our infrastructure provider and save even more money, but I also think it’s more painful to leave it going if it isn’t going to do anything. Another consideration I’ve had to make is that if I am doing this, that means I am not doing something else. The human brain can really only work on one thing at a time. Even when we believe we’re multitasking, our brains are really just rapidly switching back and forth between the two tasks, giving the illusion of doing two things at once. Of course in actuality, that means that neither task is being done as well as it could be.&lt;/p&gt;

&lt;p&gt;So taking everything above into consideration, the emotional cost of watching Neptune slowly fail, versus pulling the plug now based on the data we have. The fact that working on this means not working on something else that could have more potential, and of course the money being spent on something that has no returns. The decision was difficult, of course, but it also became the obvious decision after a while.&lt;/p&gt;

&lt;h3 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h3&gt;

&lt;h4 id=&quot;do-your-research-first&quot;&gt;Do your research first&lt;/h4&gt;

&lt;p&gt;It’s probably not a huge surprise, but everyone who says talk to potential customers before you build is right! I kept telling myself that Neptune was different, and these potential customers would be too hard to reach and likely wouldn’t even want to talk about something so personal anyway. All that might be true, but today if I had an idea like that, I would think very carefully about how viable the idea is if I can’t talk to anyone who might purchase it before building.&lt;/p&gt;

&lt;h4 id=&quot;it-doesnt-have-to-be-expensive-to-run-an-app&quot;&gt;It doesn’t have to be expensive to run an app&lt;/h4&gt;

&lt;p&gt;It’s actually quite inexpensive infrastructurally-speaking to run an app. Our app was less than $30/month, and it could’ve even been cheaper. We avoided AWS or anything that felt like it was going to take too much time to set up or stay on top of. Our main focus was finding a solution that would allow us to run our code as hands-off as possible. That meant managed databases and something with a simple interface. We decided to go with Render, and it was a good choice. I’ll be remaining with Render for future projects.&lt;/p&gt;

&lt;h4 id=&quot;finding-the-right-balance-is-hard&quot;&gt;Finding the right balance is hard&lt;/h4&gt;

&lt;p&gt;Through this experience, I can absolutely understand why people want to quit their jobs and go full time on a project. You cannot overestimate the time commitment of building something new and then convincing people that it is worth parting with their money. I definitely understand the dominant advice about not quitting your job until you’ve replaced, or even doubled, that income. But it’s also very exhausting to work what quickly becomes 2+ jobs. And if you add in a partner or children into the mix, it’s a compounding issue.&lt;/p&gt;

&lt;p&gt;I listened to a podcast where a married couple had built a business, and the husband/father remarked at one point that it was tough because, no matter what, you would always feel guilty. You might feel guilty because you’re spending time with kiddos, but there’s biz work to be done. But then, if you’re working on the business, you’ll feel guilty that you aren’t spending that time with your children. I found this to be exceedingly true for myself. My oldest child likes to play “Mother hen” where they try to care for everyone around them, and that includes me. On a few different occasions, I would be working late and they would come into my office to chide me for being up on the computer so late. Even in a situation where I thought I had found a balance - spend dinner and hang out time with kids, and then work after bedtime - it was still difficult to feel good about the “balancing act”.&lt;/p&gt;

&lt;h4 id=&quot;pitching-your-idea-is-harder-than-you-think&quot;&gt;Pitching your idea is harder than you think&lt;/h4&gt;

&lt;p&gt;Speaking and presenting at technical events and conferences is not at all the same as speaking and pitching a business idea. This one took me a long time to learn. I had been talking to my friends about Neptune for years, so I was very used to talking about the pros and cons of different tech stacks, or where to host the project, or how to manage the document generation piece. But when you’re pitching a business, no one cares about that at much. Truthfully, people mostly care about if you can make money. After that, they care about how much money you can make, and then how much revenue that turns out to be after costs. Your tech might rank fifth on the list of things people care about. This really bummed me out! I can’t say I was exclusively interested in the tech, but it ranked very high on my list, and it was something I was very comfortable talking about.&lt;/p&gt;

&lt;h4 id=&quot;data-data-data&quot;&gt;Data, data, data&lt;/h4&gt;

&lt;p&gt;People will often tell you what they think you want to hear in a conversation. If you ask someone if they think something is a good idea or if their organization would be open to partnering, most people will say yes because they don’t want to hurt your feelings. I definitely understand that instinct, but as a founder who was trying to figure out the viability of a product, it makes things really hard! If you are considering the viability of an idea, I would encourage you to keep this in mind.&lt;/p&gt;

&lt;p&gt;On the other hand, some people seem to think that the best way they can help you is to tear your idea to pieces. Some people will be really mean about it! Try not to let those people get inside your head. I’ve heard advice before along the lines of: “don’t ever listen to people who aren’t doing the work.” Obviously this advice shouldn’t be taken to an extreme, but listen to your own intuition. If it feels like this person doesn’t know what they are talking about, or if they are just being mean for the sake of being mean - listen to that feeling.&lt;/p&gt;

&lt;p&gt;But above all, listen to the data. Numbers don’t lie. If you’re doing the marketing and you’re doing the sales, and talking to potential customers, but not getting any bites? Give it a reasonable amount of time, and then reassess. Just because an idea isn’t working right away doesn’t mean it won’t work. But as I described above, an idea “working” is very relative. One person using Neptune would’ve been really amazing, but it wouldn’t have made us all of our money back. You have to have your own definition of what works, and how long your idea can take to work, and then stick to it. Take it from me, it really really hurts to continually give something more time, but then it still doesn’t work.&lt;/p&gt;

&lt;h3 id=&quot;what-happens-next&quot;&gt;What happens next?&lt;/h3&gt;

&lt;p&gt;I didn’t even realize how much of myself I had tied up in this project until we officially made the call shut it down. From our conversations, I know Harmonniey has ideas for what we can do next, but if I’m being honest, I don’t know what is next for me. Failing is incredibly hard, and having gone through it with something so important to me, its really hard to sign up for that possibility again. I think in all likelihood, we will try again on a new idea, but at this moment in time it’s a little hard to consider.&lt;/p&gt;

&lt;p&gt;I wish I had a better note to end this on. All I can say is that if you made it to the end of this post, I really appreciate you for reading. If you have any questions or want to chat, &lt;a href=&quot;https://twitter.com/carmalou&quot;&gt;please find me on Twitter.&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sun, 16 Oct 2022 00:00:00 +0000</pubDate>
        <link>carmalou.com/neptune/2022/10/16/the-end-of-neptune.html</link>
        <guid isPermaLink="true">carmalou.com/neptune/2022/10/16/the-end-of-neptune.html</guid>
        
        <category>Side-Projects</category>
        
        <category>Neptune</category>
        
        
        <category>Neptune</category>
        
      </item>
    
      <item>
        <title>Accounta-Billie Part 2: The first weekend</title>
        <description>&lt;p&gt;&lt;strong&gt;Note: This blog post references an app I no longer maintain. I let the domain go as well. I left the post up because it was a fun little project, and I enjoyed building it, as well as writing about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As I write this, I have scheduled Accounta-Billie to launch a beta this Friday, 10-23-2020. Looking back I have been working on it for about 3 months, which makes me chuckle now because my initial goal was to finish version 1 in a weekend(!). Three months as a side project is honestly pretty respectable in my opinion, especially since it is actually launching Friday, versus many, many other projects of mine that have been started but have never seen the light of day.&lt;/p&gt;

&lt;p&gt;You might be wondering why I gave in to the hype of a “weekend startup” and made that audacious goal in the first place. Firstly, I guess part of me really likes hack-a-thons. I don’t really participate in them outside of my home, but I do really like getting loads of soda and chips and staying up super late hacking away and watching Silicon Valley.&lt;/p&gt;

&lt;iframe src=&quot;https://giphy.com/embed/OqJ8UhuSyZJcfDxvsh&quot; width=&quot;480&quot; height=&quot;268&quot; frameborder=&quot;0&quot; class=&quot;giphy-embed&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://giphy.com/gifs/siliconvalleyhbo-silicon-valley-OqJ8UhuSyZJcfDxvsh&quot;&gt;via GIPHY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But beyond that, I also really believe in taking an idea from start to finish in the shortest amount of time possible. If you have an idea and you aren’t sure if it is viable, doable, whatever, its a smart idea to answer that question as soon as possible. If this is something you’re working on in your spare time, you don’t have a ton of it (that’s why it’s ‘spare’) and so it’s important to make it count. And it’s really easy to lose momentum on something that previous really excited you, especially if you’re super busy (like say, the world is in the middle of a pandemic, you’re working your full time job while doing virtual school with kids, and also generally trying to maintain some sanity). So in the interests of all of those things, I personally am a big believer in getting a project from start to finish in the shortest amount of time possible.&lt;/p&gt;

&lt;p&gt;Of course, like all things in software and life, there are about a million caveats to that. The first caveat is that it has to be reasonable. To that end, you probably aren’t going to build Facebook in a weekend. This is where I made my first mistake.&lt;/p&gt;

&lt;iframe src=&quot;https://giphy.com/embed/j0QuKSZBjch4AlIvYT&quot; width=&quot;480&quot; height=&quot;270&quot; frameborder=&quot;0&quot; class=&quot;giphy-embed&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://giphy.com/gifs/siliconvalleyhbo-comedy-season-5-j0QuKSZBjch4AlIvYT&quot;&gt;via GIPHY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here’s the thing: while I believe really strongly in taking an idea from start to finish as quickly as possible, it has to be reasonable and that means it has to be &lt;em&gt;small&lt;/em&gt;. Take your idea for an app that does all the things and start trimming. When you think it’s reasonable, trim some more, and then trim even more. You know that rule where you 3x your estimate at work? Do that here too.&lt;/p&gt;

&lt;p&gt;So if you could “probably” finish this thing in a weekend? It’s too big. Could you finish it on a Saturday before dinner? That’s the right size! There are going to be things you haven’t thought of, and styling is going to take longer than you anticipated, and so on. By getting your idea down to a single feature (or two, if you’re Wonder Woman) you’re setting yourself up for success. And if you are the unicorn everyone talks about who has perfect estimates? Well you either have some free time this weekend, or you can start on the next feature. Win-win!&lt;/p&gt;

&lt;p&gt;So now that I’ve preached the good word of keeping MVPs (minimum viable products) as small as possible, let me regale you with all the ways I did not take my own advice.&lt;/p&gt;

&lt;iframe src=&quot;https://giphy.com/embed/xUA7aO5hbPiFhp5p0k&quot; width=&quot;480&quot; height=&quot;270&quot; frameborder=&quot;0&quot; class=&quot;giphy-embed&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://giphy.com/gifs/siliconvalleyhbo-hbo-silicon-valley-xUA7aO5hbPiFhp5p0k&quot;&gt;via GIPHY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original feature list included account creation (both in-app and social login), group creation and management, multiple calendar views, multiple permissions for views (a user deciding who can see a specific task), filters for viewing only certain group member’s tasks, include both a desktop and mobile view, and make it a PWA.&lt;/p&gt;

&lt;p&gt;Phew, that is a lot of features. In hindsight, it’s easy for me to see that was never going to happen over a weekend, but I dream big. In the moment, it’s a much harder balancing act to both maintain the excitement you feel over a new project, as well as realistically determine what you can accomplish. I wish I had good advice for how to balance those two things, but the best I can really offer is to try to choose a project that you’ll use yourself. For me, that is Accounta-Billie, and knowing that it’s going to solve a problem I have is encouraging enough to keep me working on it.&lt;/p&gt;

&lt;p&gt;I started off the project pretty well doing lots and lots of planning. I had everything broken down (I’ll write another post detailing how I handled the project management.), and I think that also helped keep me excited and focused. As long as I’m talking about planning, I want to mention &lt;a href=&quot;https://github.com/philschatz/project-bot&quot;&gt;Phil Schatz’s awesome project&lt;/a&gt; for adding extra functionality to a GitHub project board. It’s a wonderful and helpful project!&lt;/p&gt;

&lt;p&gt;Over the first weekend, while I did not complete the first set of requirements, and honestly I still haven’t. A few of the features won’t be included in the beta, but will be added once the app moves to production. I ran into some troubles - especially with auth! It felt really good to get auth to completion, as well as deploying a project even though its incomplete. That’s right! It was deployed that first weekend, even without groups!&lt;/p&gt;

&lt;iframe src=&quot;https://giphy.com/embed/l46CpNxh5BHVF3t3W&quot; width=&quot;480&quot; height=&quot;270&quot; frameborder=&quot;0&quot; class=&quot;giphy-embed&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href=&quot;https://giphy.com/gifs/siliconvalleyhbo-l46CpNxh5BHVF3t3W&quot;&gt;via GIPHY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately auth did take a lot longer than I had originally intended, and so I wasn’t able to create multiple calendar views. I also didn’t finish the filters or different permissions layers. Ultimately I spent probably half of my working time on auth, which really makes my auth win that much greater!&lt;/p&gt;

&lt;p&gt;For my next project or features, I know a lot better how to plan and what to plan so that I can &lt;em&gt;actually&lt;/em&gt; complete whatever I’m working on in a weekend. Let me know what you thought of the post, and please go check out Accounta-Billie and sign up for the beta!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.twitter.com/carmalou&quot;&gt;Hit me up on Twitter!&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Tue, 20 Oct 2020 00:00:00 +0000</pubDate>
        <link>carmalou.com/accounta-billie/2020/10/20/accounta-billie-pt2.html</link>
        <guid isPermaLink="true">carmalou.com/accounta-billie/2020/10/20/accounta-billie-pt2.html</guid>
        
        <category>Side-Projects</category>
        
        <category>Progressive-Web-Apps</category>
        
        <category>Accounta-Billie</category>
        
        <category>JavaScript</category>
        
        
        <category>Accounta-Billie</category>
        
      </item>
    
      <item>
        <title>Accounta-Billie Part 1: Introducing Accounta-Billie</title>
        <description>&lt;p&gt;&lt;strong&gt;Note: This blog post references an app I no longer maintain. I let the domain go as well. I left the post up because it was a fun little project, and I enjoyed building it, as well as writing about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to take some time to write about a project I’ve been working on called Accounta-Billie. Accounta-Billie is a productivity and goals app. The concept is simple: you and your friends make up a group within the app. When one person in the group creates a task, such as “complete Accounta-Billie V1”, the whole group can see it. Once you’ve made this commitment to complete your task, your friends can help hold you accountable, and you can help hold your friends accountable to their goals as well.&lt;/p&gt;

&lt;p&gt;It may seem counter-intuitive to try to make big goals (like building an app) during this time of COVID. I’ve seen tweets about how much people have “gotten done” during this time, and other tweets imploring people to “use this time wisely”. I find that kind of gross. Life is so much more than just checking items off a checklist. During a time like this, I really think we should just let everyone be. There’s no need to use this time to complete all of your life’s accomplishments if you’re having trouble right now - or any time for that matter! Life is too short to spend your time doing anything that you don’t want to be doing.&lt;/p&gt;

&lt;p&gt;If you’re wondering how I got from the previous paragraph to “Let’s build an accountability app,” let me tell you a bit about my own quarantine experience. I started quarantine thinking, like a lot of people, that this wouldn’t last too long. I can remember thinking back in March that I only had to get to the summer, and then my kids would be in daycare, and I could I get some work done and also have my sanity back. And then when summer came and cases were worse, we made the decision to keep them home. I told myself that I only needed to get through the summer, and once school started back up, everything would go back to normal. Since then, the tough decision was made to keep my kids home and do virtual school. And even since that decision was made, my kids’ school district has decided not to have in-person classes at all.&lt;/p&gt;

&lt;p&gt;During this never-ending (and also somehow always changing) quarantine, I adopted some pretty unhealthy habits, with the idea that I was just coping with a short-term situation. I justified it, telling myself that it was only until, only until, only until…. But the habits weren’t the kinds of habits that even made you feel better. Sure, they might have helped in the short term, but, if anything, they just compounded the depressed feelings even more in the long term. So once we decided to keep my kids out of school, I knew something had to change. In that moment, I suddenly snapped out of my short term thinking and started to accept this situation as a long term sort of “new normal.”&lt;/p&gt;

&lt;p&gt;I wanted these new habits to start small, so my goal was to begin exercising regularly (of course, I didn’t know then what “regularly” meant to me), in hopes that exercise would help lift my spirits. I mentioned this to my sister, and she was really excited about the idea of us being accountability buddies. This made a lot of sense, both to help us each stick with our goals, but also because right now, we can’t see each other. We hoped that this would help keep us in contact.&lt;/p&gt;

&lt;p&gt;But we couldn’t find an app that was quite right. She suggested the typical fitness apps, but they didn’t allow you to share tasks. I suggested a spreadsheet, but she said she probably wouldn’t use anything that she couldn’t access on her phone (she also called me lame for suggesting a spreadsheet). Since we couldn’t agree on what to use, naturally, the next step was to begin building my own app. I’ll detail everything over the next few blog posts, so stay tuned!&lt;/p&gt;

&lt;p&gt;I hope you’ll check out Accounta-Billie. I’m looking for people for the beta, which is launching really soon. If you head on over to accountabillie.com, you can sign up there. I would love for you to use it, check it out, and then to get your honest feedback. Check back soon for my post about my first weekend working on Accounta-Billie and finally conquering auth.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.twitter.com/carmalou&quot;&gt;Let me know what you think on Twitter!&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Fri, 28 Aug 2020 00:00:00 +0000</pubDate>
        <link>carmalou.com/accounta-billie/2020/08/28/accounta-billie-pt1.html</link>
        <guid isPermaLink="true">carmalou.com/accounta-billie/2020/08/28/accounta-billie-pt1.html</guid>
        
        <category>Side-Projects</category>
        
        <category>Progressive-Web-Apps</category>
        
        <category>Accounta-Billie</category>
        
        <category>JavaScript</category>
        
        
        <category>Accounta-Billie</category>
        
      </item>
    
      <item>
        <title>How to manage navigation in Xamarin</title>
        <description>&lt;p&gt;As someone who loves JavaScript as much as I do, I never expected to be working in Xamarin. But that’s where I’ve been since I started my awesome new job! Xamarin brings some interesting challenges. You might think it’s just regular old C#, but since Xamarin uses &lt;a href=&quot;https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm&quot;&gt;MVVM architecture&lt;/a&gt; versus &lt;a href=&quot;https://dotnet.microsoft.com/apps/aspnet/mvc&quot;&gt;MVC architecture&lt;/a&gt;, I found a few things very tricky to manage. In this blog post, we’ll talk about how I managed navigation in a master detail app.&lt;/p&gt;

&lt;p&gt;So in this app we used a master-detail app, which included a hamburger menu. The master property contained the hamburger menu, while the detail contained whichever partial view page was meant to be the current content. The code behind included a built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Navigation&lt;/code&gt; property. You can use it like this:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;PushAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;MyPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;According to &lt;a href=&quot;https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm#view&quot;&gt;Xamarin docs&lt;/a&gt;, most or all of our app’s logic should be moved to the ViewModel. You can access the navigational properties via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App.Current.MainPage.Navigation&lt;/code&gt; and push pages to the stack, as well as remove pages. However, I ran into a problem where the hamburger menu was disappearing when I would use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App.Current.MainPage.Navigation&lt;/code&gt;. I realized I needed to find a way to actually update the Detail property itself. Unfortunately the Detail property doesn’t seem to be easily accessible. So I added a new constructor to the MainPage like this:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// MainPage.xaml.cs&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// MainPage constructor&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;MainPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;myPage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Detail&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;NavigationPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Activator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreateInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;myPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// FlowerPage.xaml.cs&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// call MainPage page and pass in page, and any parameters&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FlowerDetailPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AssemblyQualifiedName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;I am param&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MainPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;PushAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;MainPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This works well enough, however it seems a little yucky to keep adding new MainPages to the navigation stack. It seems like it would be better to directly update the Detail property, but how can we access it?&lt;/p&gt;

&lt;p&gt;We ended up creating a navigational client with a method like this:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// NavigationClientManager.cs&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SetDetail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;MainPage&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mpage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MainPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NavigationPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MainPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CurrentPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;detailPage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Activator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreateInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mpage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Detail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;PushAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detailPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// FlowerPage.xaml.cs&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// set the detail page&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;I am param&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NavigationClientManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetDetail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FlowerDetailPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This allows the ViewModel to simply call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetDetail&lt;/code&gt; method, and avoid all of the casting of types in the nav client. It really cleans up the ViewModels and makes it very clear what is going on.&lt;/p&gt;

&lt;p&gt;If you have another way of managing navigation in a master-detail app, I would love to hear it. &lt;a href=&quot;https://www.twitter.com/carmalou&quot;&gt;Let me know what you think!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shout out to my friend &lt;a href=&quot;https://github.com/danksalot&quot;&gt;Jacob Danks&lt;/a&gt; who helped me come to this solution. Thanks Jacob!&lt;/strong&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 20 Apr 2020 00:00:00 +0000</pubDate>
        <link>carmalou.com/xamarin/2020/04/20/xamarin-master-detail-nav.html</link>
        <guid isPermaLink="true">carmalou.com/xamarin/2020/04/20/xamarin-master-detail-nav.html</guid>
        
        <category>C#</category>
        
        <category>C-Sharp</category>
        
        <category>Xamarin</category>
        
        <category>Mobile-development</category>
        
        <category>Mobile</category>
        
        <category>Hamburger-menu</category>
        
        <category>Navigation</category>
        
        
        <category>Xamarin</category>
        
      </item>
    
      <item>
        <title>Caches: Caches.match</title>
        <description>&lt;p&gt;Arguably, the biggest part of service workers is being able to fetch data off of a user’s machine, rather than making trips across the network. There’s a variety of reasons reaching over the network isn’t ideal – lack of connection, intermittent connection, speed of the network. Fortunately service workers help with all of these things.&lt;/p&gt;

&lt;p&gt;In this post, we’ll look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caches.match&lt;/code&gt; – the function to fetch data from a service worker.&lt;/p&gt;

&lt;p&gt;There are two matching methods within the Cache API – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caches.match&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caches.match&lt;/code&gt; requires at least one parameter, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt; has no required parameters. First, let’s dig into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt; can only be run on a single cache. It has two optional parameters – request and options.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;caches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cacheName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
	&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;rez&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;matchAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the cache contained a key-value pair matching that request, the associated response would be returned. If there were no match, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rez&lt;/code&gt; will be undefined.&lt;/p&gt;

&lt;p&gt;The steps look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. Check that the request exists as a key in the list

2. If it does, add it to a response list

3. Return list of responses
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;sub&gt;Source: &lt;a href=&quot;https://w3c.github.io/ServiceWorker/#dom-cache-matchall&quot;&gt;Service worker spec&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;

&lt;p&gt;You can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt; without a request, and it will return &lt;em&gt;all&lt;/em&gt; the responses in the specified cache.&lt;/p&gt;

&lt;p&gt;So what’s the difference between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caches.match&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.matchAll&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caches.match&lt;/code&gt; must be run with a request parameter. Additionally it’ll check over each and every cache in the app. It might look like this:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;rez&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;caches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately checking over each cache can result in it being a little slow, especially if there are multiple caches created for an app. It can be better to specify a cache and search it specifically, or even only have only a single cache. That would look really similar to our code up above with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;matchAll&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;caches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cacheName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
	&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;rez&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 01 May 2019 00:00:00 +0000</pubDate>
        <link>carmalou.com/lets-take-this-offline/2019/05/01/caches-dot-match-under-the-hood.html</link>
        <guid isPermaLink="true">carmalou.com/lets-take-this-offline/2019/05/01/caches-dot-match-under-the-hood.html</guid>
        
        <category>Offline-First</category>
        
        <category>Service-worker</category>
        
        <category>Connectivity</category>
        
        <category>Internet-Connection</category>
        
        <category>JavaScript</category>
        
        <category>Lets-take-this-offline</category>
        
        <category>Cache-Storage</category>
        
        <category>Cache-API</category>
        
        <category>Caches.match</category>
        
        
        <category>Lets-take-this-offline</category>
        
      </item>
    
  </channel>
</rss>
